Skip to content

Agents ​

An agent is a coding agent, such as Claude Code or codex, that your workflow opens and gives work to. You ask it something in a turn, it works in your repository as it always does, and you get back an outcome: a checked answer in the shape you asked for, or a reason it has none. This page is how you call an agent and what comes back. The words agent, turn, outcome and harness are defined in How it works.

A first agent ​

ts

import { typed } from "agentswf/typebox";
import { defineWorkflow } from "agentswf/workflow";
import Type from "typebox";

export const SCOPE = typed(
  Type.Object(
    {
      files: Type.Array(Type.String(), {
        description: "Every file the fix would change.",
      }),
      risk: Type.Union([
        Type.Literal("low"),
        Type.Literal("medium"),
        Type.Literal("high"),
      ]),
    },
    { additionalProperties: false },
  ),
);

export default defineWorkflow({
  meta: {
    name: "scope-issue",
    description: "Ask an agent what fixing ISSUE.md would touch.",
  },
  async run(wf) {
    const scoper = await wf.agents.open({
      key: "scoper",
      runtime: "claude",
      instructions:
        "You read code to answer questions about it. You never change it.",
    });
    const { outcome } = await scoper.run({
      prompt:
        "Read ISSUE.md. Which files would its fix change, and how risky is it?",
      schema: SCOPE,
    });
    if (!outcome.answered) return wf.stop(`${outcome.kind}: ${outcome.reason}`);
    return outcome.value; // { files: string[]; risk: "low" | "medium" | "high" }
  },
});

Saved as scope-issue/workflow.ts, the folder layout awf init makes, it runs with:

sh
awf run scope-issue/workflow.ts

Three calls do the work:

  • wf.agents.open starts a claude agent named scoper in this run, with standing instructions.
  • scoper.run gives it one turn: the prompt, and the schema its answer must fit.
  • outcome.answered tells a checked answer from every other ending, and types outcome.value as the schema says.

wf.stop ends the run with a reason, and wf.log adds a line to its log: both are on Workflows and in the reference.

Opening an agent ​

ts
const agent = await wf.agents.open({ key: "scoper", runtime: "claude", instructions: "…" });
  • key names the agent in this run, in its record and its pane's tab. Opening the same key again returns the same agent; opening it with a different spec rejects.
  • runtime picks the harness and model: an alias, "claude" or "codex", or spelled out, { harness: "codex", model: "gpt-6-luna", effort: "high" }, or an alias with your own effort, { alias: "codex", effort: "high" }. TUI or headless, and which models and efforts each harness takes, are on Harnesses.
  • instructions are standing instructions. They go ahead of the agent's first prompt, so they stay in its session for every turn after.
  • skills, sandbox, cwd and tools: Skills are below; Sandboxes and Tools have pages of their own. cwd is where the agent works, the run's working directory by default.

An agent stays open until the run ends, a turn of it fails or times out, or you stop it. When it has nothing more to do, await scoper.close() closes it for good, so its pane doesn't sit idle; opening that key again rejects.

A typed answer ​

A turn with a schema ends answered only with a value that fits it. The agent hands its answer back with wfa result, the engine checks it, and a value that doesn't fit is refused with the reason. The agent fixes it and sends it again within the same turn. What the agent sees shows the command it is taught.

ts
const SCOPE = typed(Type.Object({ files: Type.Array(Type.String()) }, { additionalProperties: false }));
const { outcome } = await scoper.run({ prompt, schema: SCOPE });
  • Write the schema with TypeBox, or any library that produces JSON Schema, or as a plain JSON Schema object.
  • typed(...), from agentswf/typebox, gives outcome.value its TypeScript type. A bare TypeBox schema doesn't carry one by itself, so without it the value is untyped JSON. It returns the schema unchanged.
  • Without a schema the answer is a string: whatever the agent hands back with wfa result.
  • additionalProperties: false on every object refuses fields you didn't ask for.

The engine checks a small subset of JSON Schema: one whose every rule an agent can act on when it is refused.

TypeWhat it can say
objectproperties, required, additionalProperties: true | false
arrayitems, minItems
stringconst, enum, minLength
number, integerconst, minimum, maximum
booleanconst
nullnothing more
anyanyOf, a bare enum, and notes such as description, title, default, examples

Any other keyword, such as pattern, format or maxLength, rejects the turn before the agent is asked: output schema keyword "pattern" is unsupported.

What each ending means ​

run resolves to { outcome }. A turn ends exactly once, and every ending is a value, never an exception. outcome.kind is one of six:

kindWhat happenedThe agent after itWhat you usually do
answeredIt handed back a value that fits the schema: outcome.value.openUse the value.
unansweredIt stopped working without an accepted answer, even after a nudge asked it for one.openAsk again, differently, or give up on this piece.
blockedIt is showing a question only a person can answer, such as a permission prompt.openStop, or let someone answer it in its pane.
timed-outThe turn's deadline passed first.closedStop, or open a new agent with more time.
failedSomething broke: the harness crashed, the model errored, or there is no login.closedStop. A missing login is below.
cancelledThe run, an enclosing scope, or agent.close() stopped it.open after a scope's cancel; closed for good after close()Nothing: your workflow or the run is already stopping it.

Every kind but answered carries outcome.reason, a sentence for a person and the run's record, such as agent settled without an accepted result. Branch on kind, not on the reason's words.

A turn that fails or times out closes its agent. The next run, compact, set or fork on it rejects with logical agent is closed, for the rest of the run; opening its key again returns the same closed agent, so a fresh agent needs another key. After unanswered or blocked the agent is still open, its session intact.

A missing login fails the turn with outcome.login: which harness, and what the operator runs to log in. Every agent on that harness fails the same way until they do, so stop the run rather than spend the rest of it:

ts
if (outcome.kind === "failed" && outcome.login) wf.stop(outcome.reason);
// outcome.login.run: "run `claude`, then /login; …"

failed also carries retryable, whether trying again might help.

run rejects only for a mistake in the workflow, such as an unsupported schema or a closed agent, or when the deadline around the whole scope has passed. A turn's own timeout is an outcome.

An agent that stops without answering gets a nudge before the turn ends unanswered. timeout bounds a turn, nudges included, and the nudge option changes or turns them off: Timeouts.

Several turns ​

Each run on the same agent continues its session, so the agent remembers what it read, did and answered. A second turn can build on the first without repeating it:

ts
const planner = await wf.agents.open({
  key: "planner",
  runtime: { alias: "codex", effort: "high" },
});
const first = await planner.run({
  prompt: "Read ISSUE.md and plan its fix.",
  schema: PLAN,
});
if (!first.outcome.answered)
  return wf.stop(`no plan: ${first.outcome.reason}`);
// The same session: it remembers the issue and the plan it just made.
const cut = await planner.run({
  prompt: "Cut your plan down to the smallest change that fixes the issue.",
  schema: PLAN,
});
if (!cut.outcome.answered)
  return wf.stop(`no smaller plan: ${cut.outcome.reason}`);

Turns on one agent run one at a time, in the order you call them. Different agents work at once: Parallel. An agent that does work and another that checks it, turn after turn, is a loop.

Compact, switch, fork ​

A long session fills the agent's context, and you can act on it without starting over.

compact runs the harness's own compaction, as typing /compact with a focus does. The prompt says what to keep; the next turn goes on in the compacted session.

ts
const compacted = await planner.compact({
  prompt:
    "Keep the final plan and the files it names. Drop the files' contents.",
});
if (!compacted.answered)
  wf.log("compaction didn't finish", { reason: compacted.reason });

compact, like set, returns an outcome, not { outcome }. answered means it compacted, with the harness's summary as the value where it shows one; any other outcome leaves the context as it was, so going on is usually right.

fork opens new agents on a copy of this agent's session, so each starts knowing what it knew, without being told again and with that context read from the provider's cache. From then on, neither sees the other's turns.

ts
const reviewers = await Promise.all([
  planner.fork({
    key: "security",
    instructions: "You look for security problems.",
  }),
  planner.fork({
    key: "tests",
    instructions: "You decide what tests a change needs.",
  }),
]);
const reviews = await Promise.all(
  reviewers.map(async (reviewer) => {
    const { outcome } = await reviewer.run({
      prompt: "Review your plan. One line per problem.",
    });
    return outcome.answered
      ? outcome.value
      : `${reviewer.key}: ${outcome.kind}`;
  }),
);

A fork has its parent's harness, model, effort, working directory, sandbox and skills, and may name its own instructions, and a runtime of placement, metered and effort, never a harness or model. It has its parent's tools unless it names its own tools, which replace them ({} gives none): Tools. It rejects before the parent's first turn.

set switches the model, the effort or both for every later turn, as /model and /effort do. The session and its context stay.

ts
await planner.set({ effort: "low" });
const title = await planner.run({
  prompt: "Write a pull request title for the plan.",
});

agent.execution shows the settings now in force. set rejects before it runs for an effort the harness doesn't list, or where the harness can't switch. Switching the model costs the cache: the next turn reads the whole context again at full price.

Which harness does each, in its TUI or headless, is in the reference: compact, set, fork.

Instructions and skills ​

Instructions are what the agent is for, said once: "You review for security; you never change code." Put what changes per turn in the prompt.

Skills are folders holding a SKILL.md, which the agent finds by their description and uses when a task fits. skills lists exactly the skills an agent gets:

ts
const writer = await wf.agents.open({
  key: "writer",
  runtime: "claude",
  skills: [{ path: new URL("./agents-skills/changelog", import.meta.url) }],
});
  • { path } is a folder on disk. Name one beside the workflow with new URL("./skills/name", import.meta.url); a relative path is refused.
  • { repo: "owner/repo", skill: "name", ref: "v1.2" } is a public skill from GitHub or a git URL, fetched at ref, or at the default branch as it is when the run starts. Pin a ref for runs you compare.
  • skills: [] gives none. Left out, the agent has the skills it would have without awf: yours on the host, none in a sandbox.

Each agent gets a copy of its own, so an agent can't change the skill another one runs. A skill that can't be found or read, or a SKILL.md without a name and description in its frontmatter, makes open reject. The harness's own bundled skills, and claude's and codex's skills in the working directory, stay either way. cursor takes skills only in a sandbox: on the host, a cursor agent with skills is refused.

Catches ​

  • A field's description reaches the agent only when the schema is sent whole. A schema of up to six plain fields (strings, numbers, booleans, a choice of words) is taught as flags, --risk low|medium|high, without its descriptions. Say what each field means in the prompt.
  • A scripted test checks your branches, not the agent. Every ending in the table above can be scripted, for free: Testing.

Next ​

  • The workflow API: Agents: every option of open, run, compact, set and fork, with its default.
  • Harnesses: claude, codex, pi and cursor; models and effort; TUI or headless.
  • The review loop: one agent implements, another reviews, until approved.