Writing workflows

A workflow is one TypeScript file with two parts: a meta export that declares the contract, and a script body that does the work. No YAML, no node editor, no framework to subclass.

The shape of a workflow

morning-digest/index.ts
import { phase, agent, output, secrets } from "@boardwalk-labs/workflow";

export const meta = {
  slug: "morning-digest",
  title: "Morning Digest",
  triggers: [{ kind: "cron", expr: "0 9 * * 1-5" }],
  permissions: { secrets: [{ name: "GITHUB_TOKEN" }] },
};

phase("Fetch issues");
const token = await secrets.get("GITHUB_TOKEN");
const res = await fetch("https://api.github.com/issues", {
  headers: { Authorization: `Bearer ${token}` },
});
const issues = await res.json();

phase("Summarize");
const digest = await agent(
  `Write a morning digest of these issues:
   ${JSON.stringify(issues)}`,
);
output(digest);

The file executes top to bottom each time the workflow runs. Top-level await is normal; deterministic code (fetching, parsing, holding secrets) and model calls (agent()) interleave freely.

The meta contract

metamust be a pure literal: Boardwalk derives the workflow's manifest from it at deploy time without executing your code. The fields:

FieldRequiredWhat it declares
nameyesThe workflow's identity; lowercase, stable across versions.
triggersyesAt least one of cron, webhook, manual. See Triggers.
permissionsnoWhat a run may do, including permissions.secrets, the allowlist of names the program may secrets.get(). See Secrets.
budgetnoCaps: max_usd, max_duration_seconds. Breaching a budget fails the run; it never silently truncates.
workspacenoDirectories under the workspace to persist between runs.
runs_onnoMachine label; defaults to boardwalk/linux.
descriptionnoOne line for the dashboard and your teammates.

The SDK

Everything a program imports comes from @boardwalk-labs/workflow (MIT, source). The same imports work on every engine.

agent()

const text = await agent("Summarize this changelog: ...");

// Name a model explicitly, or pass a schema for structured output:
const groups = await agent<Groups>(prompt, {
  model: "anthropic/claude-sonnet-4.5",
  schema: GROUPS_SCHEMA,
});

Runs one model call to completion and resolves to its final text (or, with schema, the validated object). model is optional: on Boardwalk, omitting it lets the platform route the call (no API key needed); under boardwalk dev and self-hosting it uses your configured default model and your key. provider selects a named inference provider when you have several.

phase() and output()

phase("name") marks a section of the run for the live tail and the run log; it is observability only. output(value)records the run's result, which is what the dashboard, notifications, and workflows.call() see.

sleep()

await sleep(5 * 60 * 1000); // ms
await sleep({ until: "2026-07-01T09:00:00Z" });

Really sleeps: the process waits and your locals survive. There is no checkpointing or replay to reason about; a sleeping run simply holds its machine until it wakes.

workflows.call() and parallel()

// Durable child run: the parent waits and gets the child's output.
const result = await workflows.call("file-issue", { title });

// Fan out in-process work:
await parallel(items.map((item) => () => handle(item)));

workflows.call() starts another workflow as a durable child run and is idempotent: if the parent restarts, it re-attaches to the same child instead of spawning a duplicate. workflows.run() is the fire-and-forget variant.

Crashes and restarts

If a run's process dies, Boardwalk restarts the program from the top, like a Lambda or a CI job. Write accordingly: make side effects idempotent where it matters, and put work you must not repeat behind workflows.call() (which re-attaches) rather than inline.