# Review loop

**The problem:** an agent writes code, and you want a second agent to check it against rules the
first wasn't given, and have it fixed until it passes, without a person relaying feedback.

**The workflow:**
[`examples/messaging`](https://github.com/romanlv/agentswf/tree/main/examples/messaging). An
implementer writes `slugify()` in a fresh folder. A reviewer that knows a house style for slugs
runs the code on three titles, messages the implementer the fixes it needs, waits for each, and
runs the code again. The workflow then checks the reviewer's slugs against the known answers.

From a clone of the repository, after `bun install`:

```sh
bun awf run examples/messaging/workflow.ts            # both agents on codex
bun awf run examples/messaging/workflow.ts -- claude-tui
```

<!-- TODO(operator): record a run -->
**Cost and time:** not recorded yet. They come from a recorded run of the example, priced at list
prices; [Cost](/guide/cost) has where an estimate comes from.

## The workflow

```ts
import {
  defineWorkflow,
  type ExecutionConfig,
  type OutputSchema,
  type WorkflowInvocation,
} from "agentswf/workflow";

// An implementer writes slugify() in a fresh folder; a reviewer who knows the house style it was
// not told runs the code, asks for the fix by message, and checks again. Nothing to set up:
//
//   awf run examples/messaging/workflow.ts [-- codex | claude | pi | claude-tui]
//
// Both agents run the runtime named, codex when none is; each is one that can wait for a reply.

export const RUNTIMES = {
  codex: { harness: "codex", model: "gpt-6-luna", placement: "headless" },
  pi: {
    harness: "pi",
    model: "openrouter/deepseek/deepseek-v4.1-flash-1017",
    placement: "headless",
  },
  claude: { harness: "claude", model: "claude-sonnet-5-5", placement: "headless", metered: true },
  "claude-tui": { harness: "claude", model: "claude-sonnet-5-5" },
} as const satisfies Record<string, ExecutionConfig>;
export type RuntimeName = keyof typeof RUNTIMES;

/** What the house style makes of each title; the implementer is told none of it. */
export const CASES = [
  { title: "Hello, World!", slug: "hello_world" },
  { title: "  Crème Brûlée Recipes ", slug: "creme_brulee_recipes" },
  { title: "awf 2.0: Agents Talk", slug: "awf_2_0_agents_talk" },
] as const;

export const BUILT: OutputSchema<{ file: string }> = {
  type: "object",
  properties: { file: { type: "string" } },
  required: ["file"],
  additionalProperties: false,
};

export const REVIEW: OutputSchema<{
  approved: boolean;
  outputs: { title: string; slug: string }[];
}> = {
  type: "object",
  properties: {
    approved: { type: "boolean" },
    outputs: {
      type: "array",
      items: {
        type: "object",
        properties: { title: { type: "string" }, slug: { type: "string" } },
        required: ["title", "slug"],
        additionalProperties: false,
      },
    },
  },
  required: ["approved", "outputs"],
  additionalProperties: false,
};

export type MessagingResult = {
  folder: string;
  approved: boolean;
  /** Each slug the reviewer got by running the code, beside the house style's. */
  checks: { title: string; expected: string; got?: string }[];
  /** The implementer's turns that a message started. */
  revisions: number;
};

export const messaging = defineWorkflow<{ runtime: RuntimeName }, MessagingResult>({
  meta: {
    name: "messaging",
    description: "A reviewer asks an implementer for a fix by message and waits for it.",
  },
  async run(wf, { runtime }) {
    const execution = RUNTIMES[runtime];
    const folder = `/tmp/awf-messaging/${wf.runId}`;
    const implementer = await wf.agents.open({ key: "implementer", runtime: execution });
    const reviewer = await wf.agents.open({
      key: "reviewer",
      runtime: execution,
      instructions: [
        "You review code against the house style for slugs: lowercase ASCII, accents dropped,",
        "every run of other characters one underscore, none at either end.",
        ...CASES.map(({ title, slug }) => `${JSON.stringify(title)} -> ${slug}`),
      ].join("\n"),
    });
    await wf.messages.allow({
      from: reviewer,
      to: implementer,
      purpose: "Ask the implementer to fix slugify() until it meets the house style.",
      maxMessages: 4,
    });

    const built = await implementer.run({
      label: "Implement",
      prompt: [
        `Create the folder ${folder} and write ${folder}/slug.mjs in it, exporting`,
        "slugify(title), which turns a title into a URL slug. Plain JavaScript, no dependencies.",
        "Work only in that folder.",
      ].join(" "),
      schema: BUILT,
    });
    if (!built.outcome.answered) return wf.stop(`implementer: ${built.outcome.reason}`);

    const review = await reviewer.run({
      label: "Review",
      prompt: [
        `Review ${built.outcome.value.file}. Run slugify() on each title in your instructions`,
        "with node. If any slug is wrong, message the implementer the house style's rule and, for",
        "each wrong slug, its title, the slug it got and the slug expected; wait for its reply,",
        "then run them all again. Return the slugs you got on your last run.",
      ].join(" "),
      schema: REVIEW,
    });
    if (!review.outcome.answered) return wf.stop(`reviewer: ${review.outcome.reason}`);

    const { approved, outputs } = review.outcome.value;
    return {
      folder,
      approved,
      checks: CASES.map(({ title, slug }) => {
        const got = outputs.find((output) => output.title === title)?.slug;
        return got === undefined ? { title, expected: slug } : { title, expected: slug, got };
      }),
      revisions: wf
        .usage()
        .filter((operation) => operation.agent === "implementer" && operation.cause).length,
    };
  },
  prepare: parseRuntime,
  present: ({ folder, approved, checks, revisions }) =>
    [
      `${approved ? "approved" : "not approved"} after ${revisions} revision${revisions === 1 ? "" : "s"} by message; code in ${folder}`,
      ...checks.map(
        ({ title, expected, got }) =>
          `${got === expected ? "right" : "wrong"}  ${JSON.stringify(title)} -> ${got ?? "(not run)"}`,
      ),
    ].join("\n"),
});

function parseRuntime({ argv }: WorkflowInvocation): { runtime: RuntimeName } {
  const [runtime = "codex", ...rest] = argv;
  if (!Object.hasOwn(RUNTIMES, runtime) || rest.length > 0) {
    throw new Error(`messaging: -- [${Object.keys(RUNTIMES).join(" | ")}]`);
  }
  return { runtime: runtime as RuntimeName };
}

export default messaging;
```

Its test is [`workflow.test.ts`](https://github.com/romanlv/agentswf/blob/main/examples/messaging/workflow.test.ts).

## Why it's shaped this way

- **The reviewer owns the loop.** The code makes two calls, `implementer.run` and
  `reviewer.run`, and every round of review and fix happens before `reviewer.run` resolves. The
  reviewer sends the fixes with `wfa send --await-reply implementer` and ends its turn; the
  message starts an implementer turn, and the implementer's reply starts the reviewer's next turn,
  in the same session. `reviewer.run` resolves when the reviewer hands back its answer. Your code
  doesn't count rounds or carry findings between prompts. [Messaging](/guide/messaging) has how a route works.
- **The route is the bound.** `wf.messages.allow` lets only the reviewer start a message, and
  `maxMessages: 4` caps the conversation, replies included: two rounds of fix and answer. Past
  that the reviewer can't ask again and has to return what it has.
- **The reviewer knows what the implementer doesn't.** The house style is in the reviewer's
  `instructions` only. A reviewer that holds the acceptance criteria, ideally on another model,
  doesn't share the implementer's blind spots.
- **The message carries the evidence.** The prompt has the reviewer send the rule and, for each
  wrong slug, the title, the slug it got and the one expected. The implementer was never told the
  house style, so a message that only says "fix it" leaves it guessing.
- **The reviewer checks evidence, not prose.** It runs the code and reports the slugs it got. The
  workflow then compares them with `CASES` itself, so `approved` is the reviewer's opinion and
  `checks` is the fact, and a wrong slug the reviewer let through still shows `wrong`.
- **Every turn's outcome is checked.** A turn that didn't answer stops the run with its reason
  rather than reading a value that isn't there.
- **The record says what happened.** `revisions` counts the implementer's turns that a message
  started, read from `wf.usage()`, so the result shows how many rounds it took.

## When to write the loop in code instead

The loop can live in your workflow instead: a `for` that runs the implementer, asks the reviewer
for a typed verdict, and sends the findings back, up to a number of rounds.
[Loops](/guide/parallel-and-loops#round-after-round) builds that one. Choose it when:

- the reviewer's harness or placement can't wait for a reply (a reviewer here must be one that
  can, which is why the example runs both agents headless or claude in its TUI);
- your code needs each round's findings, to log, gate or show them;
- each round should be a fresh reviewer, or several reviewers in [`parallel`](/guide/parallel-and-loops).
  [`minimum-review`](https://github.com/romanlv/agentswf/tree/main/examples/minimum-review) is
  that review step on its own: a correctness and a maintainability lens at once, claude and
  codex, each answering with typed findings.

## Catches

- **An approval is a claim.** Check what you can in code, as `checks` does here. Where you can't,
  have the reviewer report evidence (commands run, their output), not a verdict alone.
- **A review step in a larger run** wants its route granted inside a stage, so the conversation
  ends with the stage. [Plan, implement, review](/patterns/plan-implement) does that twice.
- **A loop can end unapproved.** With a bound, it has to. Give the result a field that says so,
  and what is still open, for a person or the next workflow.
- **Messages cost turns.** Each message the reviewer sends starts an implementer turn, so
  `maxMessages` bounds the cost as well as the rounds.

## See also

- [Messaging](/guide/messaging) and [Loops](/guide/parallel-and-loops#round-after-round) in the guide.
- [Messaging](/reference/workflow-api#messaging) and
  [Outcomes](/reference/workflow-api#outcomes) in the workflow API.
