# Run coding agents from your own tools: plan, then implement

> Start Claude Code or Codex on a Prized box from a script, a Slack bot or an orchestrator over HTTP, one git worktree per plan item, and get a signed webhook when each run ends: a replacement for Conductor Cloud's API, with a Python example.

URL: https://prized.dev/docs/orchestrate-coding-agents
Updated: 2026-11-05

Your tool calls the Prized API, each item of a plan becomes a Claude Code or Codex run in its own git worktree on a box, and a signed webhook tells your tool when each one ends. The runs live on the box, so nothing waits on your laptop or your bot staying up.

You need a box with your repo on it, here at `~/code/myproject` on the box `mango` ([Moving from Conductor Cloud](https://prized.dev/docs/moving-from-conductor-cloud#3-put-your-repo-on-the-box)), the agent signed in there ([Sign in first](https://prized.dev/docs/remote-agents#sign-in-first)), and an API key with **Boxes: Write** from [API keys](https://prized.dev/dashboard/workspace/api-keys). The calls below read the key and the edge's address like this ([API reference](https://prized.dev/docs/api#two-hosts-one-key)):

```bash
export PRIZED_API_KEY=dcp_…
API=https://api.prized.dev/api/v1
AUTH="Authorization: Bearer $PRIZED_API_KEY"
EDGE=$(curl -s "$API/me" -H @<(echo "$AUTH") | jq -r .edge.url)
```

## 1. Make a worktree for the item [#1-make-a-worktree-for-the-item]

A run starts in a folder that already exists (`400 invalid_path` otherwise), so add the worktree first with `POST /exec`. Append your setup script to the same command if the repo has one.

```bash
curl -s -X POST "$EDGE/v1/box/mango/exec" -H @<(echo "$AUTH") -H "Content-Type: application/json" -d '{"command":"git -C ~/code/myproject worktree add -b plan-1 ~/code/myproject-plan-1","timeoutSeconds":120}' | jq '{exitCode, stderr}'
```

## 2. Start a run in it [#2-start-a-run-in-it]

```bash
curl -s -X POST "$EDGE/v1/box/mango/prompts" -H @<(echo "$AUTH") -H "Content-Type: application/json" -d '{"provider":"claude","model":"opus","prompt":"Add rate limiting to the login endpoint. Run the tests, then commit.","cwd":"code/myproject-plan-1","auto":true}' | jq '.run | {id, status}'
```

The answer is `202 prompt.started` with the run's `id`; a paused box wakes first.

| Field | What it does |
| --- | --- |
| `provider` | `claude` or `codex` (`opencode` and `pi` work too) |
| `model` | The agent's own model id or alias, such as `opus` or `gpt-5.5`; `prized prompt models` lists them |
| `reasoningEffort` | `low`, `medium` or `high` |
| `prompt` | The task, up to 512 KiB |
| `cwd` | The worktree, relative to your home on the box |
| `auto` | No approvals, so the agent can run the tests unattended ([Approvals](https://prized.dev/docs/remote-agents#approvals)) |
| `notify` | An https URL that gets one signed POST when the run ends (step 3) |

From a shell script, `prized prompt` starts the same run, reading the task from a file:

```bash
prized prompt mango --provider claude --model opus --cwd code/myproject-plan-1 --auto --detach - < item.md
```

## 3. Get told when it ends [#3-get-told-when-it-ends]

1. The workspace owner creates the webhook signing secret once, under **Webhook signing secret** on [API keys](https://prized.dev/dashboard/workspace/api-keys). Until then, a prompt with `notify` answers `409 webhook_secret_missing` and nothing starts.
2. Add `"notify": "https://hooks.example.com/prized"` to the prompt (`--notify` on the CLI). Only public `https` addresses on port 443 are sent to.
3. Verify each request with any [Standard Webhooks](https://www.standardwebhooks.com) library and answer `2xx` quickly. Retries carry the same `webhook-id`, so dedupe on it.

The event's `data.run.status` is `done`, `failed` or `interrupted`, and `data.links.events` reads what the agent did, with the same key, while the box runs. The full body and the retries are on [API reference: Webhooks](https://prized.dev/docs/api#webhooks).

## Fan out: one run per plan item [#fan-out-one-run-per-plan-item]

Repeat steps 1 and 2 for each item, with its own worktree and branch. A prompt without `conversationId`, `continue` or `queue` starts a new conversation, so the items run side by side.

* **On one box:** up to one run in progress per 2 GB of memory, at most 16 (an Extra Small runs 4); one more answers `409 prompt_limit_reached` ([Limits](https://prized.dev/docs/limits#prompting-an-agent-remotely)). The runs share the box's disk, CPU and memory, which is why each gets a worktree: two runs in one folder edit the same files.
* **A box per item:** fork a box you set up with `POST /api/v1/boxes/{id}/fork` and prompt each copy once it runs ([API reference: Boxes](https://prized.dev/docs/api#boxes)). This needs a plan, since the free credit runs one box at a time.

## Plan, then implement, in Python [#plan-then-implement-in-python]

The plan can be any list you keep. To have the agent write it, prompt it in the main checkout and copy the file back; without `--detach`, `prized prompt` returns when the run ends:

```bash
prized prompt mango --provider claude --model opus --cwd code/myproject "Plan this change as one pull request per item. Write the plan to plan.md as a Markdown list, one line per item starting with '- '. Change nothing else."
```

```bash
prized cp mango:code/myproject/plan.md plan.md
```

`orchestrate.py start` makes a worktree and starts a run for each line of the plan that begins with `- `; `orchestrate.py serve` is the webhook receiver, which prints each run's status and the agent's last message.

```python title="orchestrate.py"
import json, os, re, sys, threading, urllib.error, urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from standardwebhooks import Webhook

AUTH = {"Authorization": "Bearer " + os.environ["PRIZED_API_KEY"], "Content-Type": "application/json"}
REPO = os.environ.get("REPO", "code/myproject")  # the repo's folder on the box, under your home

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args):  # urllib would resend the key to wherever a redirect points
        return None  # so a 3xx raises HTTPError instead

def call(url, body=None):
    req = urllib.request.Request(url, json.dumps(body).encode() if body else None, AUTH)
    try:  # a long wait: a box in deep sleep takes minutes to wake
        with urllib.request.build_opener(NoRedirect).open(req, timeout=600) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"{url}: {e.code} {e.read().decode()}") from None

def start(plan_file, notify_url, first=1):
    edge = call("https://api.prized.dev/api/v1/me")["edge"]["url"] + "/v1/box/" + os.environ["BOX"]
    items = [line[2:].strip() for line in open(plan_file) if line.startswith("- ")]
    for n, item in list(enumerate(items, 1))[first - 1:]:
        name = f"plan-{n}-" + re.sub(r"[^a-z0-9]+", "-", item.lower())[:30].strip("-")  # branch and folder
        tree = f"{REPO}-{name}"
        made = call(f"{edge}/exec", {"command": f"test -d ~/{tree} || git -C ~/{REPO} worktree add -b {name} ~/{tree}", "timeoutSeconds": 120})
        if made["exitCode"] != 0:
            raise RuntimeError(made["stderr"])
        prompt = f"{item}\n\nYou are in a git worktree on branch {name}. Run the tests, then commit."
        run = call(f"{edge}/prompts", {"provider": "claude", "model": "opus", "prompt": prompt,
                                        "cwd": tree, "auto": True, "notify": notify_url})["run"]
        print(f"{name}: {run['id']}")

class Receiver(BaseHTTPRequestHandler):
    seen = set()  # a retried delivery keeps its event id

    def do_POST(self):
        body = self.rfile.read(int(self.headers["Content-Length"]))
        try:
            data = self.hook.verify(body, dict(self.headers))["data"]
        except Exception:
            return self.send_error(401)
        self.send_response(204)
        self.end_headers()
        if data["run"]["id"] and data["eventId"] not in self.seen:  # id is null for a queued prompt that never ran
            self.seen.add(data["eventId"])
            threading.Thread(target=report, args=(data["run"], data["links"])).start()

def report(run, links):
    try:  # a run's events can be read while its box runs
        count = call(links["run"])["run"]["eventCount"]
        events = call(f"{links['events']}?after={max(count - 20, 0)}")["events"]
        said = [e["text"] for e in events if e.get("text")][-1:]  # the agent's last words
    except Exception as e:
        said = [f"(could not read the run: {e})"]
    print(run["id"], run["status"], *said, flush=True)

if __name__ == "__main__":
    try:
        if sys.argv[1] == "start":
            start(sys.argv[2], sys.argv[3], int(sys.argv[4]) if len(sys.argv) > 4 else 1)
        else:
            Receiver.hook = Webhook(os.environ["PRIZED_WEBHOOK_SECRET"])
            HTTPServer(("", int(sys.argv[2])), Receiver).serve_forever()
    except RuntimeError as e:
        sys.exit(str(e))
```

```markdown title="plan.md"
- Add a subtract(a, b) function to calc.py, with a test.
- Add a multiply(a, b) function to calc.py, with a test.
- Add a README.md that says what calc.py does and how to run the tests.
```

Both halves read `PRIZED_API_KEY`, exported as above, and need one library beyond Python's own:

```bash
pip install standardwebhooks
```

The receiver speaks plain HTTP, so run it behind your https address on port 443, with the signing secret from step 3:

```bash
PRIZED_WEBHOOK_SECRET=whsec_… python orchestrate.py serve 8080
```

Then, from anywhere:

```bash
BOX=mango python orchestrate.py start plan.md https://hooks.example.com/prized
```

It starts every item at once, so use a box whose limit covers the plan, and prints a run id per item; the receiver prints a line as each run ends. If it stops partway (a missing secret, the box's limit), fix that and start again from the first item it did not print, such as `python orchestrate.py start plan.md https://hooks.example.com/prized 4`. Each run commits on its own branch; to have it open a pull request too, add that to the prompt (`gh` on the box is signed in if it was on your machine at setup).

Once a branch merges, remove its worktree and branch on the box:

```bash title="you@box ~/code/myproject"
git worktree remove ../myproject-plan-1-add-a-subtract-a-b-function-to
git branch -D plan-1-add-a-subtract-a-b-function-to
```

## Coming from Conductor's API [#coming-from-conductors-api]

| Conductor Cloud | Prized |
| --- | --- |
| `POST /v0/workspaces`, or `conductor workspace create` | `POST /exec` to add a worktree (and run your setup script), then `POST /prompts` with `cwd` set to it |
| `projectId` or `repositoryUrl` | The repo, cloned on the box once (`prized setup . --clone`, or an [environment](https://prized.dev/docs/environments)'s repos) |
| `branch` | The branch `git worktree add` checks out or starts from |
| `agent`: `claude` or `codex` | `provider` (`--provider` on the CLI): `claude` or `codex` |
| `model`, `effort` | `model`, in the agent's own ids (`opus`, `gpt-5.5`), and `reasoningEffort` (`--effort`): `low`, `medium` or `high` |
| `message`, or `conductor workspace create --message-file item.md` | `prompt`; `prized prompt … - < item.md` reads it from a file |
| `env` | `env` on `POST /api/v1/boxes`, or the box's **Settings** tab: the box's [variables](https://prized.dev/docs/environments#per-box-vars), for every run on it |
| Polling `GET /v0/sessions/{id}/status` | The `notify` webhook, or `GET /prompts/{runId}` |
| `GET /v0/sessions/{id}/messages?after=…` | `GET /prompts/{runId}/events?after=…`, one run's events after a `seq`; `follow=1` streams them |
| `POST /v0/sessions/{id}/messages`, a follow-up | `POST /prompts` with the run's `conversationId` and the same `cwd`; add `queue: true` while a turn is running |
| `POST /v0/sessions/{id}/cancel` | `POST /prompts/{runId}/interrupt`; prompts queued behind it wait for the conversation's next turn |
| `POST /v0/workspaces/{id}/archive` | `git worktree remove` through `POST /exec`; it deletes the folder but keeps the branch, so commit first |
| `POST /v0/workspaces/{id}/sleep` | `POST /api/v1/boxes/{id}/suspend`, which pauses the whole box; on a box that stops clean when it pauses, that ends its runs ([Webhooks](https://prized.dev/docs/api#webhooks)) |

* **Shared box:** each Conductor workspace was isolated; the runs on a box share it, up to the limit above.
* **Agents and efforts:** Conductor's `cursor` and `acp` agents, and its efforts other than `low`, `medium` and `high`, have no match in the prompt API.
* **No routines:** nothing on Prized starts a saved prompt from an incoming URL, so have the endpoint you already run call `POST /prompts`.
* **No chat view:** follow a run with `prized events --run pr_… --follow`. Runs don't message each other; your orchestrator passes results along.
* **A task per request:** for a Slack bot or a queue that sends one task at a time rather than a plan, [Run Conductor-style agent tasks from code](https://prized.dev/docs/conductor-api-agent-tasks) takes each from request to pull request.
