Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.bastani.ai/llms.txt

Use this file to discover all available pages before exploring further.

runWorkflow is the composition root. Call it from inside any CLI library — Commander, citty, yargs, an OpenTUI app — and a workflow runs. There is no Commander adapter, no wrapper to opt into.
import { runWorkflow } from "@bastani/atomic-sdk/workflows";
import workflow from "./workflows/review-to-merge/claude.ts";

const { id, tmuxSessionName } = await runWorkflow({
  workflow,
  inputs: { target_branch: "main" },
});

API

interface RunWorkflowOptions {
  workflow: WorkflowDefinition;
  inputs?: Record<string, string>;
  detach?: boolean;
  pathToAtomicExecutable?: string;
}

interface RunWorkflowResult {
  id: string;              // session ID
  tmuxSessionName: string; // e.g. atomic-wf-claude-review-to-merge-a1b2c3d4
}

async function runWorkflow(options: RunWorkflowOptions): Promise<RunWorkflowResult>;
OptionDescription
workflowThe compiled workflow from defineWorkflow(...).compile().
inputsValidated against the declared inputs schema before any session spawns.
detachWhen true, return immediately after starting the orchestrator session. Equivalent to the CLI’s -d / --detach.
pathToAtomicExecutableOverride the self-exec target — see Overriding the self-exec target.

Single-workflow composition root

The most common shape — one workflow, one worker file:
// src/claude-worker.ts
import { Command } from "@commander-js/extra-typings";
import {
  getInputSchema,
  runWorkflow,
  MissingDependencyError,
} from "@bastani/atomic-sdk/workflows";
import workflow from "./workflows/review-to-merge/claude.ts";

const program = new Command();
for (const input of getInputSchema(workflow)) {
  program.option(`--${input.name} <value>`, input.description ?? "");
}
program.action(async (rawOpts) => {
  try {
    await runWorkflow({ workflow, inputs: rawOpts as Record<string, string> });
  } catch (err) {
    if (err instanceof MissingDependencyError) {
      console.error(`Missing dependency: ${err.dependency}. Install it and retry.`);
      process.exit(1);
    }
    throw err;
  }
});
await program.parseAsync();
Run it:
bun run src/claude-worker.ts --target_branch=release/v2

Programmatic invocation

runWorkflow is a plain async function — you don’t need a CLI at all:
import { runWorkflow } from "@bastani/atomic-sdk/workflows";
import workflow from "./workflows/review-to-merge/claude.ts";

const { id, tmuxSessionName } = await runWorkflow({
  workflow,
  inputs: { target_branch: "main" },
  detach: true,
});
Combine with getSessionStatus(tmuxSessionName) and attachSession(id) to build your own monitoring UI.

Embedding under a parent CLI

Mount a workflow under any parent Commander tree alongside plain Commander commands:
import { Command } from "@commander-js/extra-typings";
import { getInputSchema, runWorkflow } from "@bastani/atomic-sdk/workflows";
import workflow from "./workflows/deploy/claude.ts";

const program = new Command("my-app");

const deploy = program.command("deploy").description(workflow.description);
for (const input of getInputSchema(workflow)) {
  deploy.option(`--${input.name} <value>`, input.description ?? "");
}
deploy.action(async (rawOpts) => {
  await runWorkflow({ workflow, inputs: rawOpts as Record<string, string> });
});

program.command("hello").action(() => console.log("hi"));

await program.parseAsync();
There’s no re-entry boilerplate — the SDK ships its own internal orchestrator entry script (bundled inside @bastani/atomic-sdk) and re-execs that with positional args (workflowSource, agent, base64-encoded inputs). Your CLI is never re-imported, so there’s nothing to guard against orchestrator-mode env vars. bun add @bastani/atomic-sdk is the only dependency you need — the user-facing @bastani/atomic CLI is not a peer requirement. See examples/commander-embed/cli.ts for a complete runnable version.

Overriding the self-exec target

By default the SDK self-execs into its own bundled dispatcher; pass pathToAtomicExecutable to route through a separately installed atomic binary instead. The shape is inspired by the Claude Agent SDK’s pathToClaudeCodeExecutable:
await runWorkflow({
  workflow,
  inputs,
  // Bare command name — resolved via PATH at exec time, e.g. when
  // a globally installed `atomic` is on the user's PATH.
  pathToAtomicExecutable: "atomic",
});

await runWorkflow({
  workflow,
  inputs,
  // Or an absolute path to a specific binary build.
  pathToAtomicExecutable: "/usr/local/bin/atomic",
});
Set this when you have a reason to prefer a specific atomic binary (custom build, version pinning, distribution shape) — otherwise leave it unset and let the SDK use its bundled dispatcher.

Typed errors

runWorkflow and its companions throw typed errors. Catch with instanceof to render friendly CLI output without parsing message text:
ErrorThrown when
MissingDependencyErrortmux / psmux / bun missing on PATH.
SessionNotFoundErrorUnknown session id passed to a session primitive.
WorkflowNotCompiledErrorA workflow was passed without .compile().
InvalidWorkflowErrorWorkflow file failed to load / validate.
IncompatibleSDKErrorLoaded SDK is older than the workflow’s declared minSDKVersion.
import {
  runWorkflow,
  MissingDependencyError,
  SessionNotFoundError,
} from "@bastani/atomic-sdk/workflows";

try {
  await runWorkflow({ workflow, inputs });
} catch (err) {
  if (err instanceof MissingDependencyError) {
    console.error(`Install ${err.dependency} and retry.`);
    process.exit(1);
  }
  if (err instanceof SessionNotFoundError) {
    console.error(`No such session: ${err.sessionId}`);
    process.exit(2);
  }
  throw err;
}
See examples/pane-navigation/cli.ts for a worked example.