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.