Glass Box Terrarium, explained from the inside

Last updated · Latest source on GitHub ↗

Claude CodeLesson

Workflow scripts

A Skill tells a model what to do, and the model decides each next step. When the steps must run in order, every time, for many agents, that decision is the weak point. A workflow script takes it away: the script, not the model, decides what runs next.

Know Claude Code workflows already? Skip to In Terrarium.

Outside the box · General

The concept

A Claude Code workflow is a JavaScript file that starts subagents and combines their results. It opens with a meta object naming it, then plain JavaScript with top-level await. agent() starts one subagent with a prompt, parallel() runs several at once and waits for all of them, and pipeline() runs one per item in a list. A schema on agent() makes that subagent return JSON of a given shape instead of prose. phase() groups the agents that follow in the progress view, and log() shows a message above it. Input arrives in a global named args.

export const meta = {
  name: 'audit-routes',
  description: 'Audit every route handler for missing auth checks',
}

const audits = await pipeline(args.files, (file) =>
  agent(`Audit ${file} for missing authentication checks.`, { label: file }),
)

return audits.filter(Boolean)

A runtime runs the script in the background. The script itself has no file system or shell access and takes no input from the user mid-run: its agents read, write and run commands, and its variables hold the results, so the session's context gets only the final answer. A workflow saved in a project's .claude/workflows/ runs by name in later sessions, and a permission rule such as Workflow(audit-routes) approves that one workflow by name.

Full referenceClaude Code docs: Orchestrate subagents at scale with dynamic workflows

Looking inside

In Terrarium

Terrarium's scripts sit in .claude/workflows/, each run by the that names it. The clear-backlog Skill, which works through the open-issue backlog in one run, runs workflows such as .claude/workflows/clear-backlog-explore.js, which triages each issue with its own agent, and .claude/workflows/clear-backlog-build.js, which builds and reviews each planned branch. The Skill itself keeps what a script can't do: it asks the owner its questions, waits on CI and merges.

The build script gives each branch's build agent its own worktree, and turns an agent that returned nothing into a failed result whose reason names where its commits may be:

.claude/workflows/clear-backlog-build.js ↗146–150as of
146async function buildBranch(b) {
147  // Isolated even on a re-run: if b.worktree was auto-removed, the agent starts fresh in its own.
148  const r = await agent(buildPrompt(b), { label: `build ${b.name}`, phase: 'Build', schema: WORK, isolation: 'worktree', ...modelOf(b) })
149  return r || { status: 'failed', reason: 'build agent returned nothing; its worktree may hold commits, and `pnpm check:worktrees` lists it', commits: [], decisions: [], worktree: b.worktree }
150}

When a triage agent says to build an issue the owner reserved, one by a author, or one labelled for a human, the explore script turns its answer back into a question for the owner:

.claude/workflows/clear-backlog-explore.js ↗116–119as of
116  if (r && r.outcome === 'build' && (issue.reserved || r.publicAuthor || r.readyForHuman)) {
117    r.outcome = 'grill'
118    r.question ||= { choice: 'Build this as briefed?', options: ['a) yes, as briefed', 'b) no'], pick: 'a', why: 'Reserved, Public-authored or ready-for-human: the owner decides.' }
119  }

.claude/settings.json allows them by name, and tests/unit/workflow-scripts.spec.ts checks that each one compiles and that its meta.name matches its file name.

Why it's built this way

  • A script is its Skill's code. A fixed run of agent steps needs an owner. Each script belongs to the one Skill that names it and runs it, and the treats it as that Skill's file. The cost: a script change gets a Skill change's checks, not the app's (Merge gate). ADR-0005 (docs/adr/0005-skill-model.md).
  • A real file in .claude/workflows/. Terrarium keeps each Skill in .agents/skills/<name>/ with only a link in .claude/skills/, but Claude Code loads a workflow from .claude/workflows/ by name and refuses to save one through a link. The script is a real file there instead, and its Skill names it. The cost: a Skill's code sits in two folders, joined only by that name. ADR-0005 (docs/adr/0005-skill-model.md).
  • Steps the model can't skip. The clear-backlog Skill sends many agents through triage, build and review, and a Skill's steps are only what the model decides to do next. Its agent work runs in saved workflows, so their steps can't be skipped. The cost: a restart kills a running workflow. .agents/skills/clear-backlog/SKILL.md.

What to watch for

  • Nothing lints it. A workflow script returns at the top level, so it isn't an ES module and ESLint skips its folder, and the unit test that stands in compiles every script and checks its name; Linting explains why.
  • The Gate counts it as Skill code. A change of only Skill files and paths skips the build and the browser tests; see Merge gate.
  • Plan for null. agent() returns null when you stop it or it hits an unrecoverable API error, and pipeline() keeps each null in its results. Turn each null into a result that says what to check, as the build script does, rather than dropping it.
  • It can't ask. A run takes no input once it starts. A step that needs a person, CI or a merge goes in the Skill around it, between two runs.

Try it in your project

Pick a job you hand Claude often that fans out over many files or items. Ask Claude to use a workflow for it, then save the run to .claude/workflows/ from /workflows. Add a test that compiles every script there, since your linter won't read them.

Where next

Last updated · Latest source on GitHub ↗

Something went wrong

This content couldn’t be loaded. Reloading the page usually fixes it.

Technical details
route: /t/glassbox/guide/claude-code/workflow-scripts