SDK examples

Each example is one use case, with the parts of the SDK it needs. In them, playcode is your new Playcode() client, model is a model id you chose from playcode.ai.models(), and db is your app's own database code.

On this page

An invoice reader

#

A photo or a PDF in, checked fields out. Use a model whose card has recommendedFor: "documents", and check the numbers in code:

import { z } from 'zod'

const Invoice = z.object({
  supplier: z.string(),
  number: z.string(),
  date: z.string().describe('YYYY-MM-DD'),
  lines: z.array(z.object({ text: z.string(), amount: z.number() })),
  total: z.number(),
})

export async function readInvoice(bytes: Uint8Array, mediaType: string) {
  const { object, usage } = await playcode.ai.generate({
    model: reader, // a model whose card has recommendedFor: 'documents'
    system: 'You read supplier invoices. Dates on them are day-month-year.',
    prompt: 'Read this invoice.',
    files: [{ data: bytes, mediaType }],
    schema: Invoice,
  })
  const sum = object.lines.reduce((total, line) => total + line.amount, 0)
  const addsUp = Math.abs(sum - object.total) < 0.01
  return { invoice: object, addsUp, credits: usage.credits }
}

A misread can be wrong and still add up, so let a person confirm the fields before you save them. The upload, the size checks and the costs: Read images and PDFs in your app.

A ticket sorter

#

A cheap model and a schema with fixed values turn free text into data you can filter:

const Triage = z.object({
  category: z.enum(['billing', 'bug', 'question', 'feedback']),
  urgent: z.boolean(),
  summary: z.string().describe('One sentence'),
})

for (const ticket of await db.tickets.unsorted()) {
  const { object } = await playcode.ai.generate({
    model,
    system: 'You sort the support tickets of a design-tools shop.',
    prompt: `${ticket.subject}\n\n${ticket.body}`,
    schema: Triage,
    maxOutputTokens: 300,
    user: ticket.customerId,
  })
  await db.tickets.update(ticket.id, object)
}

On a Claude model, with a long system prompt repeated over many tickets, pass cache: true. The first call writes the cache, which can cost more than plain input; each later call reads it at the cache price.

Answers from your own data

#

Give the agent a search over your own content, and ask it to name its sources:

const helpDesk = playcode.ai.agent({
  model,
  system:
    'Answer from the help articles only, and name the ones you used. ' +
    'Say so when they do not cover the question.',
  tools: {
    searchArticles: tool({
      description: 'Search the help articles; returns titles and text.',
      input: z.object({ query: z.string() }),
      execute: ({ query }) => db.articles.search(query, { limit: 5 }),
    }),
  },
})

const { text } = await helpDesk.run('Can I change my account email?')

The search can be your database's own text search, such as PostgreSQL's full-text search. Playcode has no model for embeddings yet, so vector search needs another company's embeddings API, called from the tool with your own key.

A file system over your data

#

Give the agent one customer's documents as a small file system - list, search, read, write - over your own database. It works the way coding agents work in a repository, so it needs no embeddings:

// One client's documents: the agent sees only theirs.
function fileTools(clientId: string) {
  return {
    listFiles: tool({
      description: 'List a folder: invoices, contracts or notes.',
      input: z.object({ folder: z.string() }),
      execute: ({ folder }) => db.files.list(clientId, folder),
    }),
    searchFiles: tool({
      description: 'Find documents that contain every word of the query.',
      input: z.object({ query: z.string() }),
      execute: ({ query }) => db.files.search(clientId, query),
    }),
    readFile: tool({
      description: 'Read one document by its path, as text.',
      input: z.object({ path: z.string() }),
      execute: ({ path }) => db.files.read(clientId, path),
    }),
    writeNote: tool({
      description: 'Save a note in the notes folder.',
      input: z.object({ name: z.string(), text: z.string() }),
      execute: ({ name, text }) =>
        db.files.write(clientId, `notes/${name}`, text),
    }),
  }
}

const assistant = playcode.ai.agent({
  model,
  system: 'You help a law office with client files. Name each path you use.',
  tools: fileTools(client.id),
})

db.files is your code: each path maps to a row of that client, and your database's full-text search answers searchFiles. The tools are also the agent's boundary. They take the client's id from your code, never from the model, so an instruction hidden in a document cannot reach another client's files. Put any tool that sends data out - an email, a web request - behind approval.

Refunds a person reviews

#

The agent answers customers and files refund requests, and a person on your staff decides each one:

const requestRefund = tool({
  description:
    'Ask the shop to refund an order. A person reviews every request: ' +
    'tell the customer it waits for review.',
  input: z.object({ order: z.string(), reason: z.string() }),
  execute: async ({ order, reason }, { idempotencyKey }) => {
    const key = idempotencyKey
    const request = await db.refunds.request({ order, reason, key })
    return `Refund request ${request.id} waits for review.`
  },
})

const support = playcode.ai.agent({
  model,
  system: 'You are the support desk of Paper & Co.',
  tools: { findOrder, requestRefund },
})

Your admin page lists the requests, and its approval runs the refund once, by a conditional status change. This is not approval: true: in a chat, that would let the customer approve their own refund (Who approves).

A memory of each user

#

Store facts about a user with a tool, and give them to the agent at the start of each conversation:

async function assistantFor(userId: string) {
  const notes = await db.notes.list(userId)
  const known = notes.join('\n') || 'Nothing yet.'
  return playcode.ai.agent({
    model,
    system: `You are a cooking assistant. About this user:\n${known}`,
    tools: {
      remember: tool({
        description: 'Save a lasting fact about the user, like an allergy.',
        input: z.object({ fact: z.string() }),
        execute: async ({ fact }) => {
          await db.notes.add(userId, fact)
          return 'Saved.'
        },
      }),
    },
  })
}

const assistant = await assistantFor(user.id)
const reply = await assistant.run('Quick dinner ideas?', { user: user.id })

These notes are personal data, and an allergy is health data. Tell your users what the assistant saves, and let them see and delete it.

A weekly report that survives a restart

#

A durable run keeps going through a deploy or a crash. Your scheduler starts it, and recover() picks up a run a crash cut:

const reporter = playcode.ai.agent({
  name: 'weekly-report',
  store,
  model,
  system: 'Write the weekly sales report: totals, top products, surprises.',
  tools: { salesByDay, emailTeam },
  budget: { credits: 1 },
})

await reporter.recover() // at startup, then every half minute:
setInterval(() => reporter.recover().catch(console.error), 30_000)

export const startWeeklyReport = () =>
  reporter.start('Write the weekly report and email it.', { user: 'reports' })

Key emailTeam on its idempotencyKey, so a step that runs again after a crash sends one email, not two. The store, the queue and recovery: Durable runs.

A draft streamed to the page

#

For one answer shown as it is written, stream it from your own endpoint:

app.post('/api/describe', async (c) => {
  await requireUser(c) // an endpoint anyone can call spends your credits
  const { product } = await c.req.json()
  const events = playcode.ai.stream({
    model,
    system: 'Write two sentences, plain text.',
    prompt: product,
  })
  const encoder = new TextEncoder()
  const body = new ReadableStream({
    async start(controller) {
      for await (const event of events) {
        if (event.type !== 'text') continue
        controller.enqueue(encoder.encode(event.text))
      }
      controller.close()
    },
  })
  return new Response(body, {
    headers: { 'content-type': 'text/plain; charset=utf-8' },
  })
})

For a conversation with history, tools and approvals, use the chat endpoint instead: Build a chat with the SDK.

A booking chat

#

A new Cloud app has this example running: a booking agent with a calendar tool, a booking each user approves, a fallback model, a budget per run and a daily limit per user, behind the /chat page. Its code is in backend-nest/src/ai/examples/booking-agent/. Copy the folder for your own chat, and see Build a chat with the SDK.