Skip to main content
Not every step needs a model call. TOON supports inline TypeScript for computation, API calls, data transformation, and side effects.

The run: Field

Use run: instead of prompt: to execute TypeScript:
steps[1]:
  - id: transform
    run: "const items = input.rawData.split(\",\").map(s => s.trim());\nreturn {\n  items,\n  count: items.length,\n};"
    output:
      items: "string[]"
      count: number
The run: block has access to:
  • input — the validated workflow input
  • Any upstream step outputs by their step id (e.g., analyze, research)
  • executionId, attempt, iteration — execution metadata

Return Value

The run: block must return an object matching the step’s output schema:
steps[1]:
  - id: score
    run: "const severity = analyze.severity;\nconst score = severity === \"high\" ? 10 : severity === \"medium\" ? 5 : 1;\nreturn {\n  score,\n  label: score > 5 ? \"critical\" : \"normal\",\n};"
    output:
      score: number
      label: string

Async Code

run: blocks can use await:
steps[1]:
  - id: fetch-data
    run: "const response = await fetch(`https://api.example.com/tickets/${input.ticketId}`);\nconst data = await response.json();\nreturn {\n  title: data.title,\n  status: data.status,\n};"
    output:
      title: string
      status: string

The handler: Field

For complex logic, point to an exported function in a TypeScript file:
steps[1]:
  - id: deploy
    handler: ./handlers/deploy.ts#deployToProduction
    output:
      url: string
      deployId: string
The handler function receives the same context as a run: block:
// handlers/deploy.ts
import { Effect } from "effect";

export const deployToProduction = ({ input, build }) =>
  Effect.gen(function* () {
    // Full access to Effect services
    const deployer = yield* Deployer;
    const result = yield* deployer.deploy({
      version: build.version,
      env: "production",
    });
    return {
      url: result.url,
      deployId: result.id,
    };
  });
Handler functions can return plain objects, Promises, or Effects. Smithers detects the return type and handles it appropriately.

When to Use Each

ApproachUse When
prompt:The step calls a model. Most common.
run:Short computation, data transformation, simple API calls.
handler:Complex logic, Effect services, shared code, testing.

Mixing Prompt and Code

A single step uses either prompt: or run:/handler:, not both. If you need to transform data before prompting, use two steps:
steps[2]:
  - id: prepare
    run: "const context = `${input.title}\\n\\n${input.body}`;\nconst wordCount = context.split(\" \").length;\nreturn { context, wordCount };"
    output:
      context: string
      wordCount: number

  - id: summarize
    prompt: "Summarize the following text ({prepare.wordCount} words):\n{prepare.context}"
    output:
      summary: string

Accessing Effect Services

Handler functions have full access to the Effect ecosystem:
steps[1]:
  - id: create-branch
    handler: ./handlers/vcs.ts#createBranch
    output:
      branchName: string
// handlers/vcs.ts
import { Effect } from "effect";
import { JJ } from "./services/jj";

export const createBranch = ({ input }) =>
  Effect.gen(function* () {
    const jj = yield* JJ;
    const branch = yield* jj.createBranch(input.featureName);
    return { branchName: branch.name };
  });
This is the escape hatch from declarative TOON into the full Effect builder power.

Error Handling

Errors in run: blocks follow the same retry and failure semantics as prompt steps:
steps[1]:
  - id: external-api
    run: "const res = await fetch(input.webhookUrl, {\n  method: \"POST\",\n  body: JSON.stringify({ event: \"deploy\" }),\n});\nif (!res.ok) throw new Error(`Webhook failed: ${res.status}`);\nreturn { status: \"sent\" };"
    output:
      status: string
    retry:
      maxAttempts: 3
      backoff: exponential
      initialDelay: 1s

Next Steps

  • Imports — Import handler modules and services.
  • Prompts — Prompt syntax for model-calling steps.
  • Nodes — All node kinds available in TOON.