One API
Companies
A company is a campus. You describe it in YAML documents, save them like any workflow, and apply
them. The platform compiles them into a plan and runs it: it puts items (a feature, a brief, a piece of
content) through each building's offices in order, and it stops at gates where a person decides. This
chapter is the REST surface for all of it. Base URL is https://api.plungeai.com; every route takes an ozk_
key (Authentication).
The same rules as Bots hold on every route here:
- Your rows only. The key's account is the tenant. A campus another account owns answers the same
404 company_not_foundas one that does not exist. - Four wires. JSON by default.
POST/PUTbodies may be YAML, andformat=orAcceptpicks the response format (json,yaml,markdown,text). The event feed also streams as server-sent events. - One error shape.
{"error": {"code", "message"}}plus theX-Error-Codeheader (Errors, limits & headers).
| Method and path | What it does |
|---|---|
GET /v1/companies | your applied campuses: mode, status, building count, approvals waiting |
POST /v1/companies/{campus}/apply | compile the saved documents into the stored plan |
GET /v1/companies/{campus}/layout | the compiled company: buildings, policy, floors, intake, pipeline, offices |
GET /v1/companies/{campus}/snapshot | the company now: office states, active items, the line at the door, spend today |
GET /v1/companies/{campus}/events | the event log: a JSON page after since, or a live SSE feed (Accept: text/event-stream or ?stream=1) |
POST /v1/companies/{campus}/events | a local building's node reports its events up ({events: [...]}, 1 to 200) |
GET /v1/companies/{campus}/buildings/{building}/slice | what a local building's node pulls to run it |
POST /v1/companies/{campus}/items | put an item into a building: {building, title, body?, kind?, priority?, tier?} → 201 |
POST /v1/companies/{campus}/items/{id}/pause · resume · retry · cancel · steer | act on one item (steer takes {text}) |
GET /v1/companies/{campus}/approvals | the door: pending gates and tool permissions (status=pending|approved|rejected|expired) |
POST /v1/companies/{campus}/approvals/{ap} | decide: {decision: approve|reject, note?} |
PUT /v1/companies/{campus}/mode | {mode: supervised|tiered|autonomous}, live |
POST /v1/companies/{campus}/start · stop | arm or disarm the campus clock; both idempotent |
DELETE /v1/companies/{campus} | delete a stopped or never-started company and every row of it → 204 |
Source: orchestration/api-gateway/routes/companies.ts (the wire) over orchestration/company-service/
(every rule; contract.ts is its surface, README.md its grammar). Every response below comes from a live
walk on 2026-10-14 (UTC) against a throwaway campus that was deleted at the end. Ids are shown as ….
17.1 What a campus is
| Part | What it is |
|---|---|
| Campus | a document of kind: campus: a name, a slug, a mode and its buildings in map order |
| Building | a kind: building document, or a building written inline in the campus. It has floors of offices, a standard its policy follows, the intake items come through, and the pipeline an item walks |
| Office | an ordinary CNL workflow: the agents doing the work. It is a saved workflow name, or inline {name, workflow}. An inline office is saved for you on apply as <campus>/<building>/<office> |
| Item | one piece of work put into a building. It walks the pipeline step by step |
| Gate | a pipeline step where the standard's policy and the mode decide: pass it (auto) or ask a person (an approval) |
location: cloud buildings run their offices on the platform once the campus clock is started.
location: local buildings run on your own machine: the node pulls the building's slice and reports its
events up. The cloud clock never dispatches a local building's items.
The mode decides who passes a gate. supervised asks a person at every gate. tiered asks according to the
standard's tier table. autonomous asks only where a building's policy says require. There are two standards.
code (the software factory) has the gates spec, build_permission, diff, merge and deploy. action
(the venture studio) has the gates brief, tool_permission, content, commitment and release
(orchestration/company-service/rubrics/).
17.2 The documents: kinds campus and building
Documents are compiled, never executed. Save them with POST /v1/workflows and kind: campus or
kind: building (16.1). The walk below used one campus document with its building inline:
a cloud building whose pipeline starts with a gate, so nothing ran and nothing was spent.
kind: campus
campus:
name: plan-docs-…
slug: plan-docs-…
mode: supervised
buildings:
- name: Desk
location: cloud
policy:
tiers: action
floors:
- name: "1"
offices:
- name: draft
workflow:
description: "Desk: draft one line from the item."
tasks:
- type: harness
id: writer
agent: one-agent
goal: "Draft one line from this item. {input}"
mission: "You are the desk's writer."
allowed_tools: [task_complete]
max_iterations: 2
intake:
- api: true
pipeline:
- gate: brief
- office: draft| Key | Level | Meaning |
|---|---|---|
campus.name · campus.slug | campus | the slug is the {campus} of every route (^[a-z0-9][a-z0-9-]{0,62}$). It defaults to the slugified name |
campus.buildings[] | campus | building slugs in map order (each a kind: building document), or inline building bodies |
mode · manager | campus, building | who decides at the doors (user:<id> or team:<id>); a building overrides the campus |
building.campus | building document | the campus slug it stands on; it must equal the campus that lists it |
location · project | building | cloud or local; project is for local buildings only |
policy.tiers · policy.gates · policy.budget | building | the standard (code or action), a per-gate require / auto / tiered, and day_usd / item_usd hard stops |
floors[].offices[] | building | office names (saved workflows, runner words in a local building, or inline {name, workflow}) |
intake[] | building | {api: true} (the items route), {bot: <saved bot>}, {schedule: <cron>, office: <office>} |
pipeline[] | building | in order: {office}, {type: parallel|sequential, id, offices}, {gate}, {then: <building>} (last only) |
A document may not name machines or credentials: node, path, bindings, api_key, token, secret,
service, worker, url, endpoint and the like are refused, and an unknown key is refused with its path. The
full grammar is orchestration/company-service/README.md.
curl -s -X POST https://api.plungeai.com/v1/workflows \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d "$(jq -n --arg y "$(cat campus.yaml)" '{name:"plan-docs-…", kind:"campus", yaml:$y}')"
# → 201 {"workflow":{"id":"…","name":"plan-docs-…","kind":"campus","description":null,"folder":null,"updated_at":…,"version":null}}17.3 Apply
Apply reads your saved documents, compiles them and stores the plan. It also saves any inline office as a workflow. Apply again after you edit a document; an inline office the new plan no longer names is deleted.
curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/apply \
-H "Authorization: Bearer $PLUNGE_API_KEY"
# → 200 {"campus":"plan-docs-…","buildings":[{"slug":"desk","location":"cloud","offices":1}],
# "workflows":[{"name":"plan-docs-…/desk/draft","id":"…","action":"created"}],
# "schedules":[],"local":[],"warnings":[]}A document set that does not compile is 422 compile_failed, with every path: message in the message. No
saved campus document with that slug is 404 company_not_found. An inline office whose saved name
(<campus>/<building>/<office>) is already taken by an agent or a bot is 409 office_conflict
(orchestration/company-service/docs.ts).
List and layout are read-only:
curl -s https://api.plungeai.com/v1/companies -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 200 {"companies":[…,{"slug":"plan-docs-…","name":"plan-docs-…","mode":"supervised","status":"applied",
# "applied_at":"2026-10-14T00:29:15.962Z","buildings":1,"pending":0},…]}curl -s https://api.plungeai.com/v1/companies/plan-docs-…/layout -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 200 {"campus":{"slug":"plan-docs-…","mode":"supervised","manager":null,…},
# "buildings":[{"slug":"desk","location":"cloud","mode":"supervised",
# "policy":{"rubric":"action","gates":{"brief":"tiered","tool_permission":"tiered","content":"tiered",
# "commitment":"tiered","release":"tiered"},"budget":{"day_usd":null,"item_usd":null}},
# "intake":[{"api":true}],
# "pipeline":[{"id":"gate:brief","kind":"gate","gate":"brief"},
# {"id":"draft","kind":"office","offices":["draft"],"retry":null,"skip":null}],
# "connected":true,…}],
# "offices":[{"building":"desk","slug":"draft","name":"plan-docs-…/desk/draft","floor":"1","order":0,"source":"inline"}]}17.4 Start
curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/start -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 200 {"status":"running"}Once the clock is started, cloud buildings walk their items. An office step starts an engine run, which is
paid like any other run. stop disarms the clock; items keep their step and wait (17.7).
17.5 Items
curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/items \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"building":"desk","title":"Launch note","body":"One line for the launch.","kind":"content"}'
# → 201 {"id":"…","campus":"plan-docs-…","building":"desk","title":"Launch note","body":"One line for the launch.",
# "kind":"content","source":"api","priority":0,"tier":null,"step":"intake","attempt":0,"status":"active",
# "parked_reason":null,"paused":0,"steer":null,"last_output":null,"cost_usd":0,"parent_item_id":null,…}| Field | Rule |
|---|---|
building | a building slug of this campus (required) |
title | 1 to 300 characters (required) |
body | text, up to 65,536 characters |
kind | feature, bugfix, docs, research, content or task |
priority | a whole number |
tier | a tier word of the standard (T0…T3, V0…V3), up to 32 characters; without it the standard's default |
Any other field is 422 invalid_params (unknown field: …). The snapshot shows where the item is. In the
walk below it is waiting at the gate, and its approval is in the queue:
curl -s https://api.plungeai.com/v1/companies/plan-docs-…/snapshot -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 200 {"head_seq":4,"mode":"supervised","status":"running",
# "buildings":[{"slug":"desk","location":"cloud","connected":true,"mode":"supervised","active":1}],
# "offices":[{"building":"desk","office":"draft","state":"idle","item_id":null}],
# "items":[{"id":"…","title":"Launch note","step":"gate:brief","attempt":0,"status":"active",…}],
# "queue":[{"id":"…","item_id":"…","step":"gate:brief","gate":"brief","tier":"V2","status":"pending",…}],
# "truncated":false,"spend_today_usd":0,"shipped_7d":0}The five item actions are POST …/items/{id}/<action>. Each answers the item as it is now:
| Action | Effect |
|---|---|
pause | the item keeps its step and is not walked until resumed |
resume | a paused or parked item goes on at the step it stopped on (a parked item starts a fresh visit) |
retry | the step starts again as attempt + 1, or at the pipeline's retry target |
cancel | the item ends failed with parked_reason: cancelled; running office runs are not interrupted |
steer | {text} (1 to 2,000 characters after trimming): a note the next office run receives once. A blank or longer text is 422 invalid_params |
A cancelled item (status: failed) refuses pause and resume with 409 item_failed; retry it instead.
Any action on a finished item is 409 item_done (orchestration/company-service/company-service.ts).
This walk did not call the five actions. The live oracle tests/live/companies/items-actions.mjs runs all
five against the deployed API on a local building of its own.
17.6 Approvals: the door
When an item reaches a gate that asks a person, the gate opens an approval. In supervised mode every gate
asks.
curl -s "https://api.plungeai.com/v1/companies/plan-docs-…/approvals?status=pending" \
-H "Authorization: Bearer $PLUNGE_API_KEY"
# → 200 {"approvals":[{"id":"…","item_id":"…","building":"desk","step":"gate:brief","gate":"brief","tier":"V2",
# "status":"pending","payload_json":"{\"summary\":\"brief: Launch note\"}","execution_id":null,
# "requested_at":"2026-10-14T00:29:18.611Z","expires_at":"2026-10-16T00:29:18.304Z",
# "decided_at":null,"decided_by":null,"note":null}]}curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/approvals/… \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' \
-d '{"decision":"reject","note":"Docs walk: nothing to run."}'
# → 200 {"approval":{"id":"…","item_id":"…","step":"gate:brief","gate":"brief","tier":"V2","status":"rejected",
# "execution_id":null,"decided_at":"2026-10-14T00:29:19.573Z","decided_by":"user:…",
# "note":"Docs walk: nothing to run.",…}}approve moves the item on to the next step, and reject parks it (parked_reason: rejected). A gate
approval has execution_id: null. A tool permission is different: an office run paused to ask for a
tool. Approving it also continues that execution once and hands its result back to the item. The answer
then adds execution_id and continued: true, or continued: false with a continue_error (not_paused,
session_expired, execution_not_found, continue_failed). The decision stands either way. Rejecting a
tool permission leaves the run paused. A body other than {decision: approve|reject, note?} is
422 invalid_params.
17.7 Stop and delete
DELETE refuses a running company:
curl -s -X DELETE https://api.plungeai.com/v1/companies/plan-docs-… -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 409 {"error":{"code":"conflict","message":"company \"plan-docs-…\" is running — stop it first"}}curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/stop -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 200 {"status":"stopped"}
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE https://api.plungeai.com/v1/companies/plan-docs-… \
-H "Authorization: Bearer $PLUNGE_API_KEY"
# → 204
curl -s https://api.plungeai.com/v1/companies/plan-docs-…/snapshot -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 404 {"error":{"code":"company_not_found","message":"no company \"plan-docs-…\""}}Delete removes every company row at once: buildings, offices, items, runs, approvals, the event log and the
manifest. It does not remove your saved documents (the campus document and the inline offices saved as
<campus>/<building>/<office>). Find them with GET /v1/workflows?search=<slug> and delete each one with
DELETE /v1/workflows/{id} → 204 (16.1). The walk did this for both of its documents, and
afterwards the search listed none and GET /v1/companies no longer listed the campus.
17.8 Mode, events and local buildings
PUT …/mode takes {mode} and switches the whole campus live, from the next tick. It overrides the campus and
every building, a building's documented mode included, and it survives apply. Setting the campus's documented
mode clears the override. A missing or unknown mode is 422 invalid_params. GET …/events answers a JSON page of the log
after since (a seq). With Accept: text/event-stream or ?stream=1 it is a live feed of id: <seq>,
event: <type>, data: <row>. A local building's node uses two more routes with the owner's key:
GET …/buildings/{building}/slice, which returns the building's compiled pipeline, policy, project and office
ids, and POST …/events ({events: [{building, type, item_id?, office?, data?, at?}]}, 1 to 200 events) to
report back. This walk did not call these four routes. Their rules are in routes/companies.ts and
routes/companies-events.ts.
17.9 Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_format | format= is not json, yaml, markdown or text |
| 404 | company_not_found | no campus with that slug for this key, or a slug that cannot be one (answered without a lookup) |
| 404 | building_not_found · item_not_found · approval_not_found | no such building, item or approval in this company |
| 409 | conflict | DELETE while the company runs: stop it first |
| 409 | item_failed · item_done | pause or resume of a cancelled item (retry it); any action on a finished item |
| 409 | approval_not_pending | deciding an approval that is already decided or expired |
| 409 | office_conflict | apply: an inline office's saved name is taken by an agent or a bot |
| 422 | invalid_params | an item, decision, mode, steer or events body that breaks its rule; the message names the field |
| 422 | compile_failed | apply: the documents do not compile; the message lists each path: message |
| 503 | not_enabled | the deployment has no company service |
curl -s https://api.plungeai.com/v1/companies/no-such-campus/layout -H "Authorization: Bearer $PLUNGE_API_KEY"
# → 404 {"error":{"code":"company_not_found","message":"no company \"no-such-campus\""}}curl -s -X POST https://api.plungeai.com/v1/companies/plan-docs-…/items \
-H "Authorization: Bearer $PLUNGE_API_KEY" -H 'Content-Type: application/json' -d '{"building":"desk"}'
# → 422 {"error":{"code":"invalid_params","message":"title must be 1-300 characters"}}The same operations are in the API reference under Companies, one page per route.