Skip to content

API · Projects, Teams & Agents

CRUD and lifecycle for projects, roles (agents), prompts, templates, team configuration, versioning, and agent association/deployment. All endpoints are unauthenticated (see API conventions).

ProjectsController

api/projects — project CRUD, team config, import/export, and Quick Create.

Verb Route Request Response Async
GET /api/projects — 200 { projects } yes
GET /api/projects/{id} route id:int Project / 404 yes
POST /api/projects body Project 201 / 400 / 500 yes
PUT /api/projects/{id} route id:int; body Project 200 / 404 / 500 yes
DELETE /api/projects/{id} route id:int 200 / 404 yes
GET /api/projects/{id}/team route id:int TeamConfig / 404 yes
POST /api/projects/{id}/team route id:int; body TeamConfig 200 / 400 / 404 / 500 yes
PATCH /api/projects/{id}/team route id:int; body TeamDesignPatch 200 / 400 / 404 yes
GET /api/projects/{id}/export route id:int ProjectExportDto yes
POST /api/projects/import body ProjectExportDto 201 / 400 / 500 yes
POST /api/projects/quick-create body CreateProjectRequest ProjectCreationResult / 400 yes
GET /api/projects/default-path — 200 { path } yes
POST /api/projects/{id}/ensure-folder route id:int 200 / 500 yes

PATCH and POST on /team are not interchangeable

POST replaces the whole TeamConfig. Whatever the caller does not send is gone — including fields the caller never knew about, which is how a save from one screen can silently drop settings owned by another.

PATCH takes a TeamDesignPatch and merges, preserving fields the patch does not mention. Prefer it for edits.

One thing PATCH does not soften: within the patch, nodes is a whole-list replacement, not a per-node merge. Sending a patch containing one node leaves the team with one agent. The list is the edit.

Every write to /team records a version, so a mistaken save is recoverable through api/projects/{id}/versions.

Each node in a TeamDesignPatch may carry role_id, the role-library entry it was made from (the Designer's from library sets it). It is carried rather than edited: null leaves the stored value alone and an empty string clears it. Keeping it matters after the save, because the resolver reads the role's profiles through it; a reporting role saved without it would be granted edit_file by the content/code/test keyword rule.

DeliverablesController

What the run produced, as opposed to what it did. The workspace outlives the container whose ports died with it, so these are the durable view of a finished project.

Method Path Inputs Returns Auth
GET /api/projects/{projectId}/deliverables route projectId:int DeliverableTreeDto — every file in the workspace with its size, plus the detected entryPoint no
GET /api/projects/{projectId}/deliverables/raw route projectId:int; query path the file itself, 400 when the path escapes the workspace, 404 when it does not exist or any part of it is a link no
GET /api/projects/{projectId}/preview/{**path} route projectId:int, path the file rendered under a single prefix so relative assets resolve; an empty path serves the detected entry point; 404 when any part of the path is a link no

Both file routes resolve inside the workspace and refuse anything that escapes it, and both send Content-Security-Policy: sandbox — agent-written HTML must never execute on this origin. raw additionally sends X-Content-Type-Options: nosniff.

The difference between them is the whole point: raw hands back one file, while preview serves the workspace as a site. A stylesheet the page links but nobody wrote is a 404 in the browser console under preview, where under raw it would be a silently unstyled page.

RolesController

api/roles — the roles table: one row per project team member, copied when the project is created. Runs read the project's team file, not these rows. The role library is CompositionController. Paginated list and search.

Verb Route Request Response Async
GET /api/roles query page:int=1, per_page:int=50, role_type:string?, search:string? 200 { roles, pagination } yes
GET /api/roles/{id} route id:int 200 / 404 yes
POST /api/roles body Role 201 / 400 / 409 / 500 yes
PUT /api/roles/{id} route id:int; body Role 200 / 404 / 409 / 500 yes
DELETE /api/roles/{id} route id:int 200 / 404 yes
POST /api/roles/bulk body BulkRolesRequest 201 / 400 yes
GET /api/roles/search query q?, type?, tech?, specialization?, limit:int=20, page:int=1 200 { roles } yes
GET /api/roles/{id}/export route id:int RoleExportDto / 404 yes
POST /api/roles/import body RoleExportDto 201 / 400 yes

BulkRolesRequest is declared inline. See Schema · Role for the entity.

CompositionController

api/composition — the role library (Templates/composition/roles.json, plus the base roles in roles.library.json beside it), read-only: the roles templates name by role_id and Quick Create picks from. The Agent Library page and the Designer's from library read it. The library ships inside the Engine, so a role changes with an update, not through this API.

Verb Route Request Response Async
GET /api/composition/roles — List<LibraryRoleDto> yes
GET /api/composition/roles/{id}/node route id:string TeamNode / 404 no

LibraryRoleDto gives each role in file order: id, name, kind (supervisor or worker), family, purpose, description, generatorPickable (the generator is offered it today: catalogued, and its prompt names no workspace path), tools, profiles, modelSlot and the model that slot names today (empty: chosen at run time; null: no slot), requiresEvidence, home (the template it was written for, or null) and usedBy (every loaded template with a node naming it, core templates first; each { id, name, isCore }).

The node route returns the team node the role becomes, built by the same role expansion a template node with that role_id gets: name, role, description, system_prompt, tools, mcp_servers, the model from its slot, and role_id itself. It has no id (the team assigns one on save) and no topology wiring (delegate and reply come from the team it joins).

PromptsController

api/prompts — reusable prompt library.

Verb Route Request Response Async
GET /api/prompts — 200 { prompts } yes
GET /api/prompts/{id} route id:int 200 / 404 yes
POST /api/prompts body Prompt 201 yes
PUT /api/prompts/{id} route id:int; body Prompt 200 / 404 yes
DELETE /api/prompts/{id} route id:int 200 / 404 yes

TemplateController

api/template — the Workflow Template catalog and template-to-project creation. See §08 — Templates.

Verb Route Request Response Async
GET /api/template — 200 (template summaries) yes
GET /api/template/list — alias of GET /api/template yes
POST /api/template/reload — 200 { success, count, templates } yes
GET /api/template/{id} route id:string full template / 404 yes
POST /api/template/create body CreateFromTemplateRequest ProjectCreationResult / 400 (including an override that names an unknown slot) / 404 / 500 yes
POST /api/template/{id}/resolved route id:string; optional body TeamRunOverrides ResolvedTeamPreview — the team the template will run as, plus grants, removals, overrides, warnings (including a node placed on a slot the health check has marked offline, or on a slot whose model list does not include its model) / 400 { error } when an override names an unknown slot / 404 yes
POST /api/template/resolve body TeamConfig ResolvedTeamPreview for a team config in hand, with the same placement warnings / 400 no

CreateFromTemplateRequest, RunFromTemplateRequest and CreateProjectRequest accept an optional overrides object (TeamRunOverrides: modelId, nodeModels keyed by node id, preferredInstance, nodeInstances keyed by node id, subagentInstance, verifierInstance, temperature, maxTokens, contextWindow, reasoningEffort, stepRounds, toolRequests). The resolver applies it to the team before it is persisted and logs each field it set, so a benchmark pins a model without writing a template variant. modelId, preferredInstance, maxTokens, contextWindow and reasoningEffort also land on the team's subagent_defaults, so a dynamic-parallel run's spawned children follow the override rather than the operator default. The three instance fields place a run across slots, each a pin: nodeInstances sets one node's preferred_instance (winning over preferredInstance; an unknown node id is a warning), subagentInstance sets subagent_defaults.preferred_instance, and verifierInstance sets the team's verifier_instance, which places the step verifier whether verification comes from the team's block or the global switch, and the trajectory judge unless the team pins its own. POST /api/template/create, POST /api/template/{id}/resolved and POST /api/orchestrator/run-from-template answer 400 when any slot the overrides name is not a configured slot, listing the known ones. The two resolved routes are the preview of that path: nothing is created.

TeamController

api/projects/{projectId}/team — persistent-agent status (backed by the persistent-agent service).

Verb Route Request Response Async
GET /api/projects/{projectId}/team/status route projectId:int 200 { projectId, agents[] } no

TeamGeneratorController

api/teamgenerator — LLM synthesis of a team or single agent from a natural-language prompt (the Quick Create backend).

Verb Route Request Response Async
POST /api/teamgenerator/generate body string (prompt); query seed (int, optional), needs (repeatable or comma-separated: web, audio, images, video, 3d; optional), imageStyle (string, optional, read only with images), roleLibrary (bool, optional: offer the generator the base-role library; absent means yes) TeamConfig / 400 / 422 / 500 yes
POST /api/teamgenerator/generate-agent body string (prompt) TeamNode / 400 / 500 yes

422 means no design attempt could be delivered: after the generator's own repair, a node still owed something no tool it holds can make, or a stage that produces nothing could not be dropped. The body is the reasons as plain text, and Quick Create shows them in its error line.

needs is what the project needs, as ticked on the New project form. Each known value is stated in the brief the generator reads (unknown values are ignored), with imageStyle (up to 80 characters) as how every picture is drawn. A design that leaves a ticked need with no role that can make it is handed back once, through the same coverage hand-back as a requested path no role owes: web needs a role owing an .html page, audio a role holding tts_render that owes a recording, images a role holding comfyui_generate that owes pictures, and video a role owing an .mp4. A need still uncovered after the hand-back is not a failure; it is returned in the team's generation_notes. Ticking 3d asks for written briefs instead of model files, since no workspace tool makes 3D formats.

generate-agent returns one node for extending a team. When the generator leaves its model_id empty, it is filled from the same model policy the team generator uses — the planning slot for a supervisor or an agent holding delegate, the code slot for one holding run_shell or run_tests, the content slot otherwise — and stays empty when that slot is empty, which means the model is chosen at run time. A slot that names an instance and a completion cap gives the node its preferred_instance and max_tokens too, unless the node already sets them.

seed is for scripted callers that need a reproducible design: with it every design attempt samples at temperature 0 with that seed, so a rerun of the same prompt designs the same team. Without it (Quick Create sends none) the generator samples at temperature 0.3.

Bare-string bodies

Both endpoints bind a raw JSON string from the body — the request body must be a quoted JSON string ("build me a research team"), not an object.

TeamVersionController

api/projects/{projectId}/versions — version history for a project's team config (save, compare, restore, prune).

Verb Route Request Response Async
GET /api/projects/{projectId}/versions route projectId:int; query limit:int=50 List<TeamVersionSummary> yes
GET /api/projects/{projectId}/versions/{versionId} route ints TeamConfigVersion / 404 yes
GET /api/projects/{projectId}/versions/{versionId}/config route ints TeamConfig / 404 yes
GET /api/projects/{projectId}/versions/compare route projectId:int; query fromVersionId:int, toVersionId:int VersionDiff / 404 yes
POST /api/projects/{projectId}/versions/{versionId}/restore route ints TeamConfig / 400 / 404 yes
POST /api/projects/{projectId}/versions route projectId:int; body SaveVersionRequest TeamConfigVersion yes
DELETE /api/projects/{projectId}/versions/prune route projectId:int; query keepCount:int=50 200 { deletedCount } yes

SaveVersionRequest is declared inline.

Where to go next