Skip to Content

Code Workflows

Code workflows use TypeScript to define deterministic, multi-step behavior. While prompt-based workflows let the AI decide what to do, code workflows give you precise control over execution.

You don’t need to write code yourself. The Workflow Builder can generate code workflows from natural language descriptions.

When to use code workflows

Use code workflows when you need:

  • Deterministic execution — Same inputs always produce the same sequence of steps
  • Complex orchestration — Multi-step processes with branching logic
  • Precise formatting — Structured output that follows exact templates
  • Aggregation patterns — Collecting data from multiple sources before acting

Use prompt-based workflows when you need:

  • Flexible reasoning — Adapting to unexpected inputs
  • Open-ended tasks — “Do what makes sense”
  • Simple actions — Label this email, respond to this message

How code workflows work

Workflows run TypeScript in a sandboxed environment. Code is organized into named steps that enable caching and replay.

import { step, tool, complete } from './workflow-runtime'; // Step 1: Gather emails const emails = await step('gather', async () => { return await tool('search_emails', { query: 'newer_than:1d is:unread', maxResults: 100 }); }); // Step 2: Send summary await step('send', async () => { const list = emails.map(e => `- ${e.subject}`).join('\n'); await tool('send_email_to_user', { subject: `Daily Summary: ${emails.length} emails`, body: `Here are your unread emails:\n\n${list}` }); }); complete();

Core API

step(name, fn)

Execute a named step. Steps are the unit of caching and replay.

const result = await step('step_name', async () => { // Your code here return someValue; });

Rules:

  • All logic must be inside step callbacks
  • Step names must be unique within a workflow
  • Steps execute in order; each waits for the previous to complete

tool(name, params)

Call a tool. Same tools available to prompt-based workflows.

const emails = await tool('search_emails', { query: 'from:newsletter', maxResults: 50 });

invokeWorkflow(options)

Call a named AI agent for tasks needing intelligence. See Calling Agents.

const result = await invokeWorkflow({ agent: { type: "name", name: "summary_writer" }, context: { emails, count: emails.length }, sync: true }); if (!result.success) throw new Error(result.error); const summary = result.result;

complete()

Signal workflow completion. Every workflow must call this at the end.

complete();

Available tools

Code workflows have access to the same tools as prompt-based workflows:

ToolDescription
search_emailsSearch Gmail
read_emailRead specific email content
add_labelAdd label to email
archive_emailArchive email
send_email_to_userSend email to you
query_session_historyQuery what other workflows have done
list_calendar_eventsGet calendar events
create_calendar_eventCreate events with attendees
web_searchSearch the web

Example: Daily summary

import { step, tool, invokeWorkflow, complete } from './workflow-runtime'; // Gather filtered emails from session history const data = await step('gather', async () => { const result = await tool('query_session_history', { agent_slug: 'town-auto-inbox', since_hours: 24, tool_names: ['add_label', 'archive_email'] }); return result.toolCalls; }); // Group by label const formatted = await step('format', async () => { if (data.length === 0) return null; const grouped = {}; for (const toolCall of data) { const label = toolCall.args?.label_name || 'Other'; if (!grouped[label]) grouped[label] = []; grouped[label].push(toolCall); } let output = ''; for (const [label, items] of Object.entries(grouped)) { output += `**${label}** (${items.length} emails)\n`; for (const item of items) { output += `- ${item.args?.subject || 'No subject'}\n`; } } return output; }); // AI writes intro/outro const parts = await step('write', async () => { if (!formatted) { return { intro: 'All quiet today!', outro: '' }; } const result = await invokeWorkflow({ agent: { type: "name", name: "summary_writer" }, context: { emailCount: data.length, list: formatted }, sync: true }); if (!result.success) throw new Error(result.error || "Summary writer failed"); return result.result; }); // Send the summary await step('send', async () => { await tool('send_email_to_user', { subject: 'Auto-inbox Summary', body: `${parts.intro}\n\n${formatted || ''}\n\n${parts.outro}` }); }); complete();

Restrictions

  1. No code outside steps — All logic must be inside step() callbacks
  2. Must call complete() — Every workflow must end with this
  3. Sandboxed environmentfs, child_process, net, etc. are blocked
  4. Tool calls may pause — Approval-required tools can pause workflow for user approval
Last updated on