Write Your First Factory

Author a you-agent-factory factory of your own, step by step, in either the declarative graph format or the JavaScript orchestrator format.

What It Is

A factory definition names three things. Work types are the categories of work that flow through it, each with an initial state, a terminal state, and a failure state. Workers are the runtimes that execute — an agent, a script, an inference call. Workstations are the steps that consume work in one state, dispatch it to a worker, and emit it into the next state. Everything else — retries, review loops, guards, fan-out, shared resources — is added to that skeleton later.

When To Use

Write your own once a packaged factory does something close to what you want but not close enough. Start from the smallest definition that runs end to end, then grow it. A factory that does not run teaches you nothing; a factory that runs badly tells you exactly what to fix next.

Choose A Format

The orchestrator field decides which engine runs your factory, and that choice decides how you author it. Graph factories describe a topology and let the engine move work through it. JavaScript factories describe a procedure and call the runtime directly. Neither is a subset of the other: the graph form gives you durable per-item state and concurrency for free, and the JavaScript form gives you ordinary control flow.
QuestionGraph factoryJavaScript factory
What do you write?Work types, workers, and workstations in JSON or YAML.One JavaScript function plus an orchestrator block.
How does control flow?Work moves between states; any workstation whose input state has work runs.Top to bottom, with ordinary await, loops, and conditionals.
Where does state live?In the work items themselves, durably, one per item.In script variables, with explicit checkpoints when you need resume.
What must you declare?Every state a work item can occupy.An argument schema and an execution policy.
Omitting the orchestrator block is legal and means graph: existing definitions load through compatibility defaulting to the Petri engine. Declare it explicitly anyway — a JavaScript factory that forgets it fails in a confusing way.

Start From A Working Example

The fastest correct start is a packaged factory you already ran. Installing one into a project-local root gives you a definition that is known to validate, known to run, and small enough to read in one sitting. @you/subagent is the smallest: one work type, one worker, one workstation.
you init --package @you/subagent --dir ./factory --replace
Read the installed definition before changing it. Renaming the workstation, pointing it at your own prompt, and running it again is a smaller first edit than writing a file from scratch, and it keeps every field that made it valid.

Write A Graph Factory

Build the definition in the order the engine reads it: declare the work before the people, and the people before the steps that use them. Each step below adds one top-level array to the file.

Step 1 — name the factory

Give the factory a name. It is the only required top-level field, and it is what the named-factory commands address later.

Step 2 — declare the work type and its states

Declare one work type with three states: an INITIAL state where submitted work arrives, a TERMINAL state that means done, and a FAILED state for work the graph could not finish. Mark it handlingBehavior DEFAULT so a portable run knows where to put the invocation input.

Step 3 — declare a worker

Declare one worker. The type decides which fields are legal on it: AGENT_WORKER accepts agent tooling and permission settings, SCRIPT_WORKER accepts a command and arguments, INFERENCE_WORKER accepts operations and model locality. Leave model and provider unset to inherit the operator defaults you configured at install.

Step 4 — declare a workstation

Declare one workstation. Its inputs name the work type and state it consumes, its outputs name the state it emits on success, and onFailure names the state it emits when the worker fails. The body is the prompt: it can interpolate invocation arguments and reference the work item being processed.

Step 5 — declare the invocation signature

Declare the invocation signature so callers know what to pass. Each parameter has an internal name, the external name used on the command line, and the bindings that accept it — positional text, piped stdin, or a named flag.

The whole file

{
  "name": "summarize",
  "description": "Summarize one submitted request in a single agent pass.",
  "workTypes": [
    {
      "name": "task",
      "handlingBehavior": ["DEFAULT"],
      "states": [
        { "name": "init", "type": "INITIAL" },
        { "name": "complete", "type": "TERMINAL" },
        { "name": "failed", "type": "FAILED" }
      ]
    }
  ],
  "workers": [
    {
      "name": "summarizer",
      "type": "AGENT_WORKER",
      "agentTools": { "policy": "READ_ONLY" },
      "skipPermissions": true
    }
  ],
  "workstations": [
    {
      "name": "summarize-request",
      "type": "AGENT_RUN",
      "worker": "summarizer",
      "inputs": [{ "workType": "task", "state": "init" }],
      "outputs": [{ "workType": "task", "state": "complete" }],
      "onFailure": [{ "workType": "task", "state": "failed" }],
      "body": "Read the request below in full and return a self-contained summary with the key claims, the evidence behind them, and anything you could not verify.\n\nRequest:\n${input}\n"
    }
  ],
  "invocationSignature": {
    "parameters": [
      {
        "name": "input",
        "externalName": "to",
        "description": "Text request to summarize.",
        "required": true,
        "bindings": [{ "kind": "POSITIONAL", "position": 1 }, { "kind": "STDIN" }, { "kind": "NAMED" }]
      }
    ]
  }
}
The eight workstation types and six worker types each accept a different field set. The schema reference lists which fields are shared, which are selected by one variant, and which are rejected.

Write A JavaScript Factory

A JavaScript factory replaces the workstation graph with a script. There are no work types, workers, or workstations to declare — the orchestrator block carries the source, the argument schema, and the safety policy, and the script itself calls agents in whatever order you write.

Step 1 — declare the orchestrator kind

Set orchestrator.kind to JAVASCRIPT. Without it the definition loads as a graph factory through compatibility defaulting, and the engine will look for workstations that are not there.

Step 2 — point at the workflow source

Point at the workflow source. Use sourceRef for a factory-relative path when the script lives beside the definition, or inlineSource when you want the definition to carry the script text directly. The packaged JavaScript factories use the inline form so they can ship as a single file.

Step 3 — declare the argument schema

Declare argsSchema as a JSON Schema object. It is what validates the arguments before the script starts, and it is what the args binding in the script is populated from.

Step 4 — set the default policy

Set defaultPolicy. It bounds what the workflow may do when no runtime override exists: how many agent calls it may make in total, how many may run at once, how deep child dispatch may nest, whether network access is allowed, and which roots are writable. READ_ONLY with an empty writable-root list is the safe starting point.

The workflow source

return (async function () {
  phase("draft");
  const draft = await agent.run({
    label: "drafter",
    prompt: "Write a first draft that answers the request in full.\n\nRequest:\n" + args.request,
  });
  if (draft.status !== "COMPLETED") {
    throw "drafting failed";
  }

  phase("review");
  const reviews = await parallel([
    {
      label: "review-accuracy",
      prompt: "Judge this draft for factual accuracy. List every unsupported claim.\n\n" + draft.output.text,
    },
    {
      label: "review-completeness",
      prompt: "Judge this draft for completeness. List every part of the request it does not answer.\n\nRequest:\n" +
        args.request + "\n\nDraft:\n" + draft.output.text,
    },
  ]);

  phase("revise");
  const final = await agent.run({
    label: "reviser",
    prompt: "Revise the draft so it survives both reviews. Return only the revised text.\n\nDraft:\n" +
      draft.output.text + "\n\nReviews:\n" + JSON.stringify(reviews.map((r) => r.output.text)),
  });
  if (final.status !== "COMPLETED") {
    throw "revision failed";
  }
  return final.output.text.trim();
})();

The definition that loads it

{
  "name": "draft-review-revise",
  "description": "Drafts an answer, reviews it from two angles, and revises once.",
  "orchestrator": {
    "kind": "JAVASCRIPT",
    "javascript": {
      "sourceRef": "workflows/draft-review-revise.js",
      "argsSchema": {
        "type": "object",
        "required": ["request"],
        "additionalProperties": false,
        "properties": {
          "request": { "type": "string", "minLength": 1 }
        }
      },
      "defaultPolicy": {
        "mode": "READ_ONLY",
        "maxAgents": 8,
        "concurrency": 2,
        "maxDepth": 1,
        "maxRetries": 0,
        "allowNetwork": false,
        "writableRoots": []
      }
    }
  },
  "invocationSignature": {
    "parameters": [
      {
        "name": "request",
        "externalName": "to",
        "description": "Request to draft, review, and revise.",
        "required": true,
        "bindings": [{ "kind": "POSITIONAL", "position": 1 }, { "kind": "STDIN" }, { "kind": "NAMED" }]
      }
    ]
  }
}
The script runs against a fixed set of runtime bindings: args and meta for invocation input, agent.run for a single child dispatch, parallel and pipeline for fan-out, phase and log for progress, and the workflow namespace for budgets, checkpoints, artifacts, and the final result.

Validate And Run

Validation is a separate step from running, and it is worth doing first: it checks the payload against the same contract the HTTP factory-validation endpoint uses, without starting a runtime. Once it passes, run the file directly as a portable factory.

Validate before running

you factory config validate ./factory/factory.json

Run it as a portable factory

you run --factory ./factory/factory.json "Summarise this repository"
A portable run needs to know which work type receives the invocation input. Factories run with --factory must declare handlingBehavior DEFAULT on exactly one work type — no more, no fewer.

Or install it under a name and run it from anywhere

you factory create summarize --from ./factory/factory.json --set-current
you run --named summarize "Summarise this repository"

Grow The File

One file stops being comfortable somewhere around the third workstation. The split layout writes canonical factory.json alongside workers and workstations directories, so each step becomes its own file and diffs stay readable.
you factory config expand ./factory/factory.json
The reverse writes the canonical single-file form back to stdout, which is useful for diffing a split layout against a definition someone sent you.
you factory config flatten ./factory

Common Pitfalls

Three mistakes account for most first-factory failures. A workstation whose input state no work ever reaches, so the run idles and exits with nothing done. A worker referenced by a name that does not match any entry in the workers array. And fields borrowed from the wrong worker type — agent tooling on a script worker, or model routing on a poller — which the schema rejects because each worker type selects its own field set.

Let A Factory Write It

A packaged factory can author factories. @you/factory-builder takes a plain-language request and produces one validated definition in whichever form you ask for, then installs it under a name you can run.
you init --package @you/factory-builder
you run --named @you/factory-builder --factory-name release-note-review --orchestrator graph \
  --to "Review submitted release notes and return an approved summary."
The orchestrator argument chooses the form the builder writes: graph for a YAML topology, javascript for a JavaScript orchestrator. It defaults to graph.

Tags