Skip to main content
Smithers is durable by design, and most of its sharp edges trace back to one fact: a run is replayed from persisted state, not from memory. The mistakes below are the ones people hit first. Each links to the canonical reference for the full story.

Resume and state

Unstable task IDs break resume

The runtime keys completed work by task id. A changed id looks like a brand new task, and an id that disappears is dropped from the plan. Derive ids from data, never from a loop index or a timestamp.
{tickets.map((t) => <TicketPipeline key={t.id} id={`${t.id}:work`} />)}
// NOT id={`work-${i}`} or id={`work-${Date.now()}`}
It is the same rule as React keys. See How It Works.

Input is immutable after the first run

A run’s --input is persisted when it starts. Resuming with different input is an error, not a silent override. If you need different input, start a new run.

Code changes block resume, they do not merge

A workflow source change is a different workflow. Resume validates the source hash of the original run, so editing the file and then resuming is blocked. Start a new run instead. To change a workflow that is still running, use hot reload (up --hot): edits apply to newly scheduled tasks while in-flight tasks finish on their original code. See Recipes.

useState is not durable

React state resets on every render, which here means every frame. Anything that must survive a crash belongs in a Task output read back through ctx, not in component state.

Caching

Do not cache side-effecting tasks

cache is for pure work that is expensive to recompute. Caching a deploy, an email, or a mutation means it silently does not run on a cache hit. The cache key is cache.by(ctx) plus cache.version plus the output schema signature, so a schema change invalidates the cache automatically and a stale cached row fails validation and misses safely. See How It Works.

Side effects and retries

Mark side-effecting tools and key them

Tasks retry, and a retried agent loop can call a tool again. A custom tool that writes to the world should declare sideEffect: true and pass ctx.idempotencyKey through to the downstream system so a retry is a no-op rather than a second charge. ctx.idempotencyKey is stable across retries and resumes for the same task iteration.
import { defineTool } from "smithers-orchestrator";
import { z } from "zod";

const placeOrder = defineTool({
  name: "shop.place_order",
  description: "Place an order",
  schema: z.object({ sku: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    return await shop.placeOrder({ sku: args.sku, idempotencyKey: ctx.idempotencyKey });
  },
});

Tools and sandbox

Agents get only the tools you grant

The five built-in tools (read, write, edit, grep, bash) are sandboxed to rootDir. Symlinks, network, and long-running calls are denied by default; --allow-network opens bash to the network. Grant least privilege per task: a reviewer gets read and grep, an implementer gets write, edit, and bash, and an agent with no tools cannot touch the filesystem at all. See How It Works.

Time travel and VCS

Revert and VCS-restoring replay change your working tree

These rewrite filesystem state, so treat them the way you would treat git checkout over uncommitted work.
  • revert restores the workspace to a previous attempt’s filesystem state and discards graph snapshots recorded after that attempt. It restores files only and lands them as a new change on top of the current working copy. See Revert to Attempt.
  • replay --restore-vcs checks out the jj revision the snapshot was taken at, so re-execution sees the same source as the original run.

revert requires jj

Smithers prefers .jj over .git. Pure Git repos run fine but cannot use revert, because there is no per-attempt change to restore. Install jj if you want attempt-level revert. See VCS.

Worktree runs auto-rebase on resume

On resume of a worktree run, Smithers rebases onto the base branch (default main) and continues even if the rebase fails. Expect the branch to move.

Outputs

ctx.outputMaybe is undefined until the task runs

Reading a downstream output before its task has completed returns undefined, not a default. Guard it so a not-yet-run task does not crash the render.
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
return analysis ? <Task id="report" output={outputs.report} agent={writer}>...</Task> : null;
  • How It Works: the execution model these rules come from.
  • Recipes: caching, hot reload, and VCS revert in context.