SDK reference

A workflow program imports everything from @boardwalk-labs/workflow (MIT, source). These hooks are facades over the engine running the program, so the same imports work identically under boardwalk dev, self-hosting, and Boardwalk. The package also exports the TypeScript types (WorkflowMeta, AgentOptions, and more) and the manifest schema for tooling.

agent()

function agent<T = string>(prompt: string, opts?: AgentOptions): Promise<T>

Runs one model call to completion. Without a schema it resolves to the final text; with one, to a validated object (pass the type: agent<Bug[]>(prompt, { schema })).

interface AgentOptions {
  model?: string;       // omit → the managed Auto lane; or an opaque "<vendor>/<model>" id
  provider?: string;    // default "boardwalk" (managed); name your own for BYO keys
  reasoning?: ReasoningEffort | ReasoningOptions;  // "none".."xhigh", or { effort, maxTokens, exclude }
  schema?: JsonSchema;  // JSON Schema → structured, validated output
  name?: string;        // a label for this agent in the run log
  builtins?: "all" | "read-only" | "none" | string[];  // which built-in tools (read/write/edit/bash/grep)
  cwd?: string;         // the workspace subdirectory this agent works from (default: the root)
  tools?: ToolDef[];    // extra program-defined tools, added on top of the built-ins
  mcp?: McpServerRef[]; // MCP servers this agent may call
  skills?: string[];    // skills from the package's skills/ dir to load
  memory?: string;      // a workspace dir for this agent's persistent memory
  humanInput?: boolean; // let the agent pause mid-loop to ask a person (off by default)
}

Model and provider are chosen per call; the workflow declares none. See Inference. Capabilities are per call too: each agent() brings its own tools, MCP servers, skills, and memory; there are no workflow-level capability fields.

reasoning controls how hard the model thinks before answering. A bare string is an effort level, "none" through "xhigh" (reasoning: "high" is shorthand for { effort: "high" }); the object form also takes maxTokens (a direct cap, for providers that take one) and exclude(think internally but keep the trace out of the response). Omit it for the provider's adaptive default. The one neutral control maps to each provider's native knob, so a model that doesn't support a level surfaces an error rather than a silent downgrade.

Tools, MCP, skills

Every agent()already has a working tool belt. By default it can read, edit, and run things in the run's workspace with no setup:

// The default built-ins, all on unless you narrow them:
// read, write, edit, ls, grep, glob, bash, apply_patch, webfetch, web_search, artifacts, lsp, subagent

Scope them with builtins: "read-only" drops the mutating tools (handy for an agent that reads untrusted input), "none" removes them all, or pass an explicit list of names. Add your own tools on top: each is a typed function that runs in your program (the trusted layer), so its return value, not a model guess, comes back:

One built-in stands apart: subagent lets an agent spawn a child agent to isolate or fan out a subtask. The child runs one level deep (it gets no subagenttool of its own) with at most the parent's tools, and returns only its result, keeping the parent's context clean. It shares the run's budget.

const result = await agent("Open the highest-priority ticket and draft a reply.", {
  builtins: "read-only",
  tools: [
    {
      name: "get_ticket",
      description: "Fetch a ticket by id.",
      inputSchema: {
        type: "object",
        properties: { id: { type: "string" } },
        required: ["id"],
      },
      execute: async ({ id }) => fetchTicket(id as string), // real code, real data
    },
  ],
});

Point an agent at an external MCP server with mcp, and load reusable instructions from your program package's skills/ directory with skills:

await agent("Triage and label this issue.", {
  mcp: [
    { name: "linear", transport: "http", url: "https://mcp.linear.app", headers: { Authorization: `Bearer ${token}` } },
  ],
  skills: ["triage-rubric"], // skills/triage-rubric.md, shipped in the package
  memory: "notes",           // a workspace dir this agent keeps across runs
});

An MCP server is either { transport: "http", url, headers? } or { transport: "stdio", command }. On hosted runners only HTTP servers are reachable (a remote endpoint, authenticated with a header you can build from a secret); stdio is for local servers under boardwalk dev and self-hosting. The connected tools are namespaced <server>__<tool>so two servers can't collide.

memory is a workspace-relative directory the agent reads and writes; it is persisted across runs automatically, so an agent can accumulate notes over time. These are all per call: two agent() calls in the same workflow can carry entirely different tools, servers, and skills.

cwd points an agent at a workspace subdirectory: its file tools resolve and stay inside that directory, bashstarts there, and the agent is told that directory's layout. A run that clones three repos can give each agent one checkout, so every path in its prompts and findings is clean and repo-relative:

await parallel(repos.map((repo) => () =>
  agent(`Fix the failing tests in this repo.`, { cwd: `checkouts/${repo}` })
));

The directory must already exist (clone or mkdir it in program code first). memory stays relative to the workspace root, and a subagent inherits its parent's cwd.

input, output, config

const input: unknown                         // the trigger payload (a value, not a function)
const config: Readonly<Record<string, JsonValue>>  // deploy-time configuration
function output(value: JsonValue): void      // declare the run's result

input is whatever started the run (a webhook body, --input, or a workflows.call argument). output()sets the run's result: what the dashboard, notifications, and a calling parent receive (last call wins). config is a frozen object supplied at deploy time.

secrets.get()

function secrets.get(name: string): Promise<string>

Resolves a secret to its plaintext value, fail-closed against permissions.secrets. The value is redacted from everything the model sees. See Secrets & environments.

runtime

runtime.runId       // string — this run's id
runtime.workflowId  // string
runtime.orgId       // string
runtime.apiUrl      // the API origin, e.g. "https://api.boardwalk.sh"
runtime.apiToken()  // Promise<string> — a short-lived bearer for the Boardwalk API / MCP
runtime.idToken(audience)  // Promise<string> — a per-run OIDC id-token for cloud federation

The run's own identity and credentials, fetched on demand and never placed in process.env. apiToken()returns a bearer scoped to the manifest's permissions, for calling the Boardwalk API or MCP server from inside a run. idToken(audience) mints a signed OIDC id-token your own cloud can verify, the keyless alternative to storing AWS/GCP/Azure keys as secrets; it requires permissions.id_token: "write". Both are redacted from everything the model sees. See Cloud access (OIDC).

sleep()

function sleep(arg: number | { durationMs: number } | { until: string | Date }): Promise<void>

Holds the run; a bare number is milliseconds. It really waits and your locals survive. A short sleep holds in place; a long one suspends the run and releases its machine, so the wait is free, then it resumes where it left off.

humanInput()

function humanInput(opts: {
  prompt: string;
  input:
    | { kind: "text"; multiline?: boolean; placeholder?: string; required?: boolean }
    | { kind: "choice"; options: string[]; allowOther?: boolean }
    | { kind: "multiselect"; options: string[]; min?: number; max?: number };
  key?: string;          // stable id for the gate (defaults to its position)
  assignees?: string[];  // who may answer, e.g. "role:admin", "user:<id>"
  timeout?: string;      // e.g. "48h"
  onTimeout?: "fail" | { value: HumanInputResult };
}): Promise<HumanInputResult>

Pauses the run for a person to answer, then resumes with their validated response. The input form is a discriminated union: a text gate resolves to { value: string }, a choice to { value, isOther }, and a multiselect to { values, other? }. While it waits the run is suspended (it does not burn compute); a person answers from the dashboard or with boardwalk respond. See Human-in-the-loop.

workflows.call() / run() / schedule()

function workflows.call(slug: string, input: unknown, opts?: CallOptions): Promise<unknown>
function workflows.run(slug: string, input: unknown, opts?: CallOptions): Promise<string>
function workflows.schedule(
  slug: string,
  input: unknown,
  opts: { cron?: string; rate?: string; at?: string | Date; timezone?: string },
): Promise<string>

call starts another workflow as a durable child run, holds for it, and resolves to its output. runis fire-and-forget and resolves to the new run's id. Both are idempotent on (parent, target, input), so a restarted parent re-attaches instead of double-firing. schedule registers a future run (exactly one of cron, rate, or at) and resolves to a schedule id.

parallel()

function parallel<T>(thunks: readonly (() => Promise<T>)[]): Promise<T[]>

Runs thunks concurrently and resolves to their results in order: a barrier with standard Promise.all semantics (rejects on the first throw). Pass () => agent(...) thunks to fan a batch of model calls out at once.

phase()

function phase(name: string, opts?: { id?: string }): void

Marks the current section of the run for the live tail and run log. Observability only; it does not checkpoint or skip code on restart.

artifacts.write()

function artifacts.write(
  name: string,
  contentType: string,
  body: string | Uint8Array,
  metadata?: Record<string, unknown>,
): Promise<{ id: string; name: string; url: string }>

Stores a file with the run and resolves to a download URL. Use it for outputs you want to keep and link to: a report, a generated image, a diff.