Skip to main content
Learn how to use AI automation effectively by following these best practices and patterns.

When to Use AI vs Manual Automation

Use AI When:

  • Dynamic UIs - Elements don’t have stable selectors or IDs
  • Complex interactions - Multi-step workflows that are hard to script
  • Natural workflows - Actions easier to describe than code
  • Rapid prototyping - Quick proof-of-concept without precise selectors
  • Handling variations - Page structure varies based on state or user
  • Data extraction - Unstructured content needs to be parsed

Use Manual Automation When:

  • Performance critical - AI adds 1-3 seconds per call
  • High volume - Thousands of operations (cost adds up)
  • Stable selectors - Elements have reliable IDs or classes
  • Exact precision - Need pixel-perfect control
  • Offline scenarios - No internet or API access
  • Simple operations - Basic click/input on known elements

Decision Tree

Is the UI stable with reliable selectors?
├─ Yes → Use manual automation (click, input, etc.)
└─ No → Is performance critical?
    ├─ Yes → Use manual automation with resilient selectors
    └─ No → Is cost a concern?
        ├─ Yes → Use manual for bulk, AI for complex parts
        └─ No → Use AI automation
The most effective pattern combines both AI and manual automation:
import uplink from '@uplink-code/uplink'
import ai from '@uplink-code/ai'

const agent = ai.createAgent({
  provider: 'anthropic',
  options: { apiKey: process.env.ANTHROPIC_API_KEY }
})

const session = await uplink.session('<organization-api-key>', {
  projectId: '<project-id>',
  include: { ecdsa: true, ecdh: true }
})
const client = await uplink.client.fromSession(session, { agent })
const browser = await client.launch()
const page = await browser.newPage()

// Manual: Fast, reliable navigation
await page.goto('https://example.com/products')

// AI: Handle dynamic cookie banner
await page.act('Accept cookies if a banner appears')

// Manual: Precise form filling
await page.input('#search', 'wireless headphones')
await page.click('#search-button')

// AI: Complex filtering
await page.act('Filter results to show only items under $100 with free shipping')

// AI: Smart extraction
const products = await page.extract(
  'Extract the first 5 products with name, price, and rating',
  productsSchema
)

// Manual: Precise navigation
if (products.success && products.data) {
  await page.goto(products.data[0].url)
}

Prompt Engineering

Write Clear, Specific Instructions

Good prompts:
// Specific location and description
await page.act('Click the blue "Sign In" button in the top-right corner')

// Include context
await page.act('In the shipping form, select "Express Delivery" from the shipping method dropdown')

// Describe visual characteristics
await page.act('Click the red "Delete" button next to the item named "Widget Pro"')
Bad prompts:
// Too vague
await page.act('Click the button')

// Ambiguous
await page.act('Do the login thing')

// Missing context
await page.act('Select the option')

Break Down Complex Tasks

Instead of one massive instruction:
// Bad: Too complex for one instruction
await page.act('Login with username admin and password secret, then navigate to settings, update the email to [email protected], and save')

// Good: Break into logical steps
await page.act('Fill in username "admin" and password "secret", then click login')
await page.act('Click the settings link in the navigation')
await page.act('Change the email field to [email protected]')
await page.act('Click the save button')

Provide Context

Help the AI understand the page structure:
// Without context
await page.extract('Get the price', priceSchema)

// With context (better results)
await page.extract(
  'Extract the current price shown in the product details section',
  priceSchema
)

Error Handling Patterns

Graceful Fallbacks

Always have a backup plan:
// Try AI first
let result = await page.act('Click the submit button')

if (!result.success) {
  // Fallback to manual
  console.log('AI failed, trying manual automation')
  try {
    await page.click('#submit-button')
  } catch (error) {
    console.error('Both AI and manual failed')
    throw error
  }
}

Retry with Different Instructions

If extraction fails, try rephrasing:
let result = await page.extract('Get the price', priceSchema)

if (!result.success) {
  // Try a different instruction
  result = await page.extract(
    'Extract the product price shown in USD',
    priceSchema
  )
}

if (!result.success) {
  // Fall back to manual extraction
  const priceText = await page.evaluate(() =>
    document.querySelector('.price')?.textContent
  )
}

Validation and Verification

Verify AI actions worked as expected:
// Perform action
await page.act('Add item to cart')

// Verify result
const cartCount = await page.evaluate(() =>
  document.querySelector('.cart-count')?.textContent
)

if (cartCount === '0') {
  console.error('Item was not added to cart')
  // Retry or use manual method
}

Cost Management

Optimize API Usage

AI calls cost money - use them wisely:
// Bad: Multiple separate calls
await page.act('Click button A')
await page.act('Click button B')
await page.act('Click button C')

// Good: Combine when possible
await page.act('Click buttons A, B, and C in sequence')

Cache Extracted Data

Don’t extract the same data repeatedly:
// Extract once
const productData = await page.extract(
  'Extract all product details',
  productSchema
)

// Reuse the data
if (productData.success && productData.data) {
  console.log(productData.data.name)
  console.log(productData.data.price)
  // Don't call extract again for the same data
}

Use Cheaper Models for Simple Tasks

// For simple actions, use Haiku (faster and cheaper)
const cheapAgent = ai.createAgent({
  provider: 'anthropic',
  model: 'claude-haiku-4-20241024',
  options: { apiKey: process.env.ANTHROPIC_API_KEY }
})

// Use for simple tasks
page.setAgent(cheapAgent)
await page.act('Click the agree button')

// Switch to Sonnet for complex tasks
const smartAgent = ai.createAgent({
  provider: 'anthropic',
  model: 'claude-sonnet-4-5-20250929',
  options: { apiKey: process.env.ANTHROPIC_API_KEY }
})

page.setAgent(smartAgent)
await page.extract('Extract complex nested data', complexSchema)

Batch Operations

Process multiple items with fewer AI calls:
// Bad: Extract each product separately (5 AI calls)
for (let i = 0; i < 5; i++) {
  await page.goto(`/product/${i}`)
  await page.extract('Get product details', productSchema)
}

// Good: Extract all at once (1 AI call)
await page.goto('/products')
const result = await page.extract(
  'Extract details for the first 5 products',
  z.object({
    products: z.array(productSchema)
  })
)

Testing and Reliability

Test Against Real Pages

Always test with actual websites:
// Test extraction against real page
const testUrl = 'https://example.com/product/test-item'

const result = await page.extract(
  'Extract product details',
  productSchema
)

if (!result.success) {
  console.error('Extraction failed on test page:', result.error)
  // Adjust schema or instruction
}

Handle Page Variations

Pages may look different based on state:
// Check for variations
const isLoggedIn = await page.evaluate(() =>
  !!document.querySelector('.user-profile')
)

if (!isLoggedIn) {
  await page.act('Click the login button')
  // Handle login...
}

// Now proceed with main task
await page.act('Navigate to dashboard')

Set Timeouts

AI operations can take time:
// Set reasonable timeout for complex extractions
try {
  const result = await Promise.race([
    page.extract('Extract data', schema),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), 30000)
    )
  ])
} catch (error) {
  console.error('Extraction timed out, trying simpler approach')
  // Fallback strategy
}

Monitor Success Rates

Track how often AI automation succeeds:
let successCount = 0
let totalAttempts = 0

async function automateWithTracking() {
  totalAttempts++

  const result = await page.act('Perform action')

  if (result.success) {
    successCount++
  }

  console.log(`Success rate: ${(successCount / totalAttempts * 100).toFixed(1)}%`)

  if (successCount / totalAttempts < 0.8) {
    console.warn('Success rate below 80%, consider switching to manual automation')
  }
}

Schema Design

Start Simple, Then Expand

Begin with basic schemas and add complexity as needed:
// Start simple
const simpleSchema = z.object({
  title: z.string(),
  price: z.number()
})

// Test it
let result = await page.extract('Extract product info', simpleSchema)

// If successful, expand
const detailedSchema = z.object({
  title: z.string(),
  price: z.number(),
  rating: z.number(),
  reviews: z.array(z.object({
    author: z.string(),
    text: z.string(),
    rating: z.number()
  }))
})

Use Appropriate Types

Match types to the actual data format:
// If price includes currency symbol
const schema = z.object({
  price: z.string() // "$29.99"
})

// If you need numeric price, transform it
const schema = z.object({
  price: z.string().transform(str =>
    parseFloat(str.replace(/[$,]/g, ''))
  )
})

// If price is already numeric in HTML
const schema = z.object({
  price: z.number() // 29.99
})

Make Fields Optional When Appropriate

Not all data is always present:
const schema = z.object({
  // Always present
  name: z.string(),
  price: z.number(),

  // Sometimes present
  discount: z.number().optional(),
  originalPrice: z.number().optional(),
  badge: z.string().optional(), // "Best Seller", "New", etc.

  // May be null
  reviewCount: z.number().nullable(),
})

Performance Optimization

Minimize Page Context

The AI analyzes page content - simpler pages = faster processing:
// Remove unnecessary elements before extraction
await page.evaluate(() => {
  // Hide ads, footers, etc. to reduce context
  document.querySelectorAll('.ad, footer, .sidebar').forEach(el => {
    el.style.display = 'none'
  })
})

// Now extract (faster with less content)
const result = await page.extract('Extract article', schema)

Parallel Execution

Run independent operations in parallel:
// Sequential (slow)
await page.act('Accept cookies')
await page.act('Close popup')

// Parallel (faster)
await Promise.all([
  page.act('Accept cookies if present'),
  page.act('Close popup if present')
])

Pre-navigate and Cache

Navigate ahead of time:
// Pre-load pages
const pages = await Promise.all([
  browser.newPage(),
  browser.newPage(),
  browser.newPage()
])

await Promise.all([
  pages[0].goto('https://example.com/page1'),
  pages[1].goto('https://example.com/page2'),
  pages[2].goto('https://example.com/page3')
])

// Now extract from all pages in parallel
const results = await Promise.all([
  pages[0].extract('Extract data', schema),
  pages[1].extract('Extract data', schema),
  pages[2].extract('Extract data', schema)
])

Security Considerations

Protect API Keys

Never expose API keys in code:
// Bad: Hardcoded key
const agent = ai.createAgent({
  provider: 'anthropic',
  options: {
    apiKey: 'sk-ant-api03-...' // DON'T DO THIS
  }
})

// Good: Environment variables
const agent = ai.createAgent({
  provider: 'anthropic',
  options: {
    apiKey: process.env.ANTHROPIC_API_KEY
  }
})

Sanitize Instructions

If instructions come from user input, sanitize them:
// If instruction includes user input
const userQuery = getUserInput()

// Validate and sanitize
const sanitized = userQuery
  .trim()
  .substring(0, 500) // Limit length
  .replace(/[<>]/g, '') // Remove potentially harmful chars

await page.act(`Search for ${sanitized}`)

Review AI Actions in Development

Log actions during development to verify behavior:
const result = await page.act('Perform action')

// Review what AI did
console.log('Actions taken:')
result.actions.forEach(action => {
  console.log(`- ${action.type}(${JSON.stringify(action.params)})`)
})

Complete Example

Here’s a complete example following all best practices:
import uplink from '@uplink-code/uplink'
import ai from '@uplink-code/ai'
import { z } from 'zod'

// Validate environment
if (!process.env.ANTHROPIC_API_KEY) {
  throw new Error('ANTHROPIC_API_KEY required')
}

// Create agent
const agent = ai.createAgent({
  provider: 'anthropic',
  model: 'claude-sonnet-4-5-20250929',
  options: { apiKey: process.env.ANTHROPIC_API_KEY }
})

// Define schema
const productSchema = z.object({
  name: z.string(),
  price: z.number(),
  rating: z.number().optional(),
  inStock: z.boolean()
})

const session = await uplink.session('<organization-api-key>', {
  projectId: '<project-id>',
  include: { ecdsa: true, ecdh: true }
})
const client = await uplink.client.fromSession(session, { agent })

try {
  const browser = await client.launch()
  const page = await browser.newPage()

  // Manual navigation (fast)
  await page.goto('https://shop.example.com')

  // AI for dynamic content (flexible)
  await page.act('Accept cookies if a banner appears')

  // Manual search (precise)
  await page.input('#search', 'wireless headphones')
  await page.click('#search-submit')

  // Wait for results
  await page.waitForSelector('.search-results')

  // AI extraction (smart)
  const result = await page.extract(
    'Extract the first 5 products with name, price, rating, and stock status',
    z.object({
      products: z.array(productSchema)
    })
  )

  if (result.success && result.data) {
    // Process results
    result.data.products.forEach(product => {
      console.log(`${product.name}: $${product.price}`)
    })
  } else {
    // Fallback to manual
    console.error('AI extraction failed, using manual method')

    const products = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('.product')).map(el => ({
        name: el.querySelector('.name')?.textContent,
        price: parseFloat(el.querySelector('.price')?.textContent || '0')
      }))
    })
  }

  await page.close()
  await browser.close()
} finally {
  await client.close()
}