Appearance
Templates
The catalog, folders, drafts and their edits, publishing, promoting, tests and approvals.
GET /v1/templates
List templates (id, name from meta.title, folder, where published).
Each template's published maps each environment the caller may read and the template is published in to its newest published version there: the commit and when it was made (the first entry of its history). An environment it isn't published in is left out. frozen is true for a template past the licence's limit.
Responses
| Status | Description | Body |
|---|---|---|
200 | Templates | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
200 body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
templates | array of object | ||
templates[].id | string | yes | |
templates[].name | string | yes | |
templates[].folder | string | yes | |
templates[].frozen | boolean | ||
templates[].published | map of object | yes |
POST /v1/templates
Create a template draft.
Writes revision 1 of a new draft: the RFML document and any bundle files. Only the header (rfml, id, meta.title) is checked here; the engine checks the rest on the first edit or validate. 409 when a draft of that id already exists in the environment.
With Content-Type application/vnd.rfml+xml the body is the document as RFML markup (§27), parsed by the engine (/v1/markup/parse); environment and message go in the query. Markup with syntax errors is 422 with markup/* diagnostics.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
environment | query | string | ||
message | query | string | Markup bodies only. |
Request body (application/vnd.rfml+xml): string
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
environment | string | ||
document | Document | yes | |
files | object | Bundle files keyed by path (samples/…, schemas/…, tests/…). | |
message | string |
Responses
| Status | Description | Body |
|---|---|---|
201 | Created, with the draft revision | |
409 | A draft already exists, or a template in the trash holds the id: | |
422 | The header is not valid | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/import
Make a new report from a file.
The body is a report bundle's .zip (the example gallery's download, or a bundle zipped by hand; its files may sit in one top-level folder) or its template.rfml.json alone. It becomes the first draft of a new report, with the checks POST /v1/templates makes: the header, an id the trash holds, the bundle's paths and caps, and the licence's template limit. Without id, the report takes the document's id, or the first of <id>-2, <id>-3… that is free. The body is capped by limits.maxBodyBytes. Its RFML version is checked as a copy's is (POST /v1/examples/{source}/{id}/copy): an older one the engine migrates is stored migrated, and the edit says "import, RFML <from> → <to>".
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
environment | query | string | ||
id | query | string | ||
title | query | string | Sets meta.title; default the document's. | |
folder | query | string |
Request body (application/zip): string (binary)
Request body (application/json): Document
Responses
| Status | Description | Body |
|---|---|---|
201 | The new report's first draft | object |
400 | Neither a bundle's zip nor a document, or a file that isn't a bundle's | |
409 | The id is taken or in the trash, the licence's template limit is reached (licence-limit), or the document needs a newer RFML version (rfml-too-new) | |
413 | Larger than limits.maxBodyBytes | |
422 | The header is not valid, or the document is of an RFML version this Reportforge can't read (rfml-unsupported) | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
201 body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
environment | string | yes | |
revision | integer | yes | |
folder | string |
GET /v1/folders
List catalog folders.
Responses
| Status | Description | Body |
|---|---|---|
200 | Folder paths | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/folders
Create a folder path, including its parents.
Responses
| Status | Description | Body |
|---|---|---|
201 | Created | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
DELETE /v1/folders
Move a folder to the trash, with its subfolders and templates.
One trash item takes the folder, every folder under it and every template in them, each trashed as DELETE /v1/templates/{id} would. Needs template.write, and template.write in every environment when the folder holds templates. An empty folder goes to the trash too. Audited once as folder.trash, with the number of templates.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
path | query | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | object | |
403 | The folder holds templates and the caller lacks template.write in an environment | |
404 | No such folder | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
200 body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
path | string | ||
status | trashed | ||
item | TrashItem |
GET /v1/templates/{id}
Get a published template or its draft.
Returns {id, name, environment, document, commit, revision, draft, files}. With draft=true and no draft written yet, the published copy comes back with revision 0 and draft false; the first edit, sent with baseRevision 0, creates the draft.
With format=markup, or Accept: application/vnd.rfml+xml, the reply is the document alone as canonical RFML markup (§27.6, from the engine's /v1/markup/format), with X-Draft-Revision (drafts) or X-Template-Commit (published copies).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
environment | query | string | ||
draft | query | boolean | Read the draft (or the published copy when no draft is written yet). | |
commit | query | string | ||
format | query | json, markup | markup reads or writes the document as RFML markup (application/vnd.rfml+xml) instead of JSON. |
Responses
| Status | Description | Body |
|---|---|---|
200 | The template | object, string |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
DELETE /v1/templates/{id}
Move the template to the trash, from every environment at once.
Needs template.write in every environment. Each environment branch gets a commit "trash <id>" removing the template, and the trash item keeps the tree each branch had; the template store keeps the draft, its edits and history, the published copies and the assistant's conversations. The template's unfinished jobs are cancelled (template-in-trash) and its pending approvals withdrawn. The id is held until the item is restored or purged. Audited as template.trash.
Responses
| Status | Description | Body |
|---|---|---|
200 | object | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
200 body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ||
status | trashed | ||
item | TrashItem |
PUT /v1/templates/{id}/folder
Move a template into a catalog folder.
Responses
| Status | Description | Body |
|---|---|---|
200 | Folder updated | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
PUT /v1/templates/{id}/draft
Replace the draft document, whole or as a JSON Patch.
The engine diffs the new document against the draft (/v1/diff) into edit operations, which are then applied like POST …/draft/ops, so locks and "no new errors" apply. A JSON Patch (RFC 6902) is applied to the draft document first. Bundle files are written with …/draft/files/{path}.
The document can also be sent as RFML markup: Content-Type application/vnd.rfml+xml (or format=markup), with environment, baseRevision (required), dryRun and message in the query. The engine parses it (/v1/markup/parse); syntax errors are 422 with markup/* diagnostics, and nothing changes.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
format | query | json, markup | markup reads or writes the document as RFML markup (application/vnd.rfml+xml) instead of JSON. | |
environment | query | string | ||
baseRevision | query | integer (int64) | Markup bodies only (JSON bodies carry it in the body). | |
dryRun | query | boolean | Markup bodies only. | |
message | query | string | Markup bodies only. |
Request body (application/vnd.rfml+xml): string
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
environment | string | ||
baseRevision | integer (int64) | yes | |
document | Document | ||
patch | array of object | ||
patch[].op | add, remove, replace, move, copy, test | yes | |
patch[].path | string | yes | |
patch[].from | string | ||
patch[].value | any | ||
dryRun | boolean | ||
message | string |
Responses
| Status | Description | Body |
|---|---|---|
200 | Saved | DraftResult |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
412 | baseRevision is stale. The body is {error:"conflict", message, revision} with the draft's current revision. | |
422 | The engine rejected the change or the document has errors: {error:"invalid", message, diagnostics}. A lock violation is an edit/locked diagnostic. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/draft/ops
Apply a batch of RFML edit operations to the draft.
Loads the bundle, calls the engine's /v1/ops with canUnlock (the caller's template.unlock) and the "no new errors" rule, writes the result if baseRevision is still the draft's revision, and appends the batch and its inverse to the edit log. The engine is given the library releases the document resolves to (nested ones included), so imported components' props are checked; a batch that adds a library reference runs again with it resolved. Agents are rate limited by agents.maxOpsPerMinute (429).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
environment | string | ||
baseRevision | integer (int64) | yes | The revision the batch was written against; 0 when no draft exists yet. |
batch | array of Op | yes | |
dryRun | boolean | Check and report without writing. | |
message | string | For the edit log. | |
proposal | string | The id of the assistant proposal this batch accepts (POST …/assistant). When reportd relayed that proposal to the caller for this draft with exactly this batch, the edit log records the edit as the assistant agent's, on behalf of the caller (actorKind agent, source assistant), once. Relayed proposals are kept for 24 hours in the template store (file and sqlite drivers), so this holds across a reportd restart. Otherwise the edit is the caller's own. An edit that is the agent's marks the proposal accepted in the caller's conversation. |
Responses
| Status | Description | Body |
|---|---|---|
200 | Applied (or, with dryRun, would apply) | DraftResult |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
412 | baseRevision is stale. The body is {error:"conflict", message, revision} with the draft's current revision. | |
422 | The engine rejected the change or the document has errors: {error:"invalid", message, diagnostics}. A lock violation is an edit/locked diagnostic. | |
429 | An agent sent too many batches, or the write rate limit; see Retry-After |
POST /v1/templates/{id}/draft/undo
Undo the caller's latest edit that is not undone yet.
Applies the edit's stored inverse through the engine (locks apply; no batch rule, since the state it returns to may have had errors).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body, optional (application/json): UndoRequest
Responses
| Status | Description | Body |
|---|---|---|
200 | Undone; undone names the revision | DraftResult |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
409 | Nothing to undo | |
412 | baseRevision is stale. The body is {error:"conflict", message, revision} with the draft's current revision. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/draft/redo
Redo the caller's latest undone edit.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body, optional (application/json): UndoRequest
Responses
| Status | Description | Body |
|---|---|---|
200 | Redone; redone names the revision | DraftResult |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
409 | Nothing to redo | |
412 | baseRevision is stale. The body is {error:"conflict", message, revision} with the draft's current revision. | |
422 | The engine rejected the change or the document has errors: {error:"invalid", message, diagnostics}. A lock violation is an edit/locked diagnostic. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/templates/{id}/draft/edits
The draft's edit log.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
environment | query | string | ||
after | query | integer (int64) | Only edits with a higher revision. | |
limit | query | integer |
Responses
| Status | Description | Body |
|---|---|---|
200 | {edits: [{revision, actor, actorKind, onBehalfOf, source, message, kind (undo, redo or empty), target, ops, inverse, createdAt}]}, oldest first | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/templates/{id}/draft/files/{path}
Read one bundle file of the draft (or of the published copy, when there is no draft).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
path | path | string | yes | Bundle path, e.g. samples/invoice.json or assets/logo.svg. May contain slashes. |
environment | query | string |
Responses
| Status | Description | Body |
|---|---|---|
200 | The file's bytes, with the media type its name says (an asset's image or font type, application/json, else application/octet-stream); X-Draft-Revision names the revision | |
404 | No such file, or the template is in the trash (in-trash, see InTrash) | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
PUT /v1/templates/{id}/draft/files/{path}
Write one bundle file of the draft.
Samples, schemas, tests and local assets. The document changes only through edit operations, rfml.lock.json only through publish, and assets/remote/ only through publish's fetch of remote assets. JSON files must be valid JSON. Files under assets/ are images or fonts (PNG, JPEG, GIF, WebP, SVG, TTF, OTF, WOFF2): the name's extension says which, and the bytes must be that format; the body's Content-Type is not used. A document names one by its bundle path as the asset's src (assets/logo.svg); publish pins the ones it uses in rfml.lock.json by sha256, and renders, tests and previews send them to the engine by hash. Every bundle file is at most 16 MiB, and a bundle at most 64 MiB.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
path | path | string | yes | Bundle path, e.g. samples/invoice.json or assets/logo.svg. May contain slashes. |
environment | query | string | ||
baseRevision | query | integer (int64) | Optional revision check. |
Request body (application/json)
Request body (application/octet-stream)
Request body (image/*)
Request body (font/*)
Responses
| Status | Description | Body |
|---|---|---|
200 | Saved, with the new revision | |
400 | Not a bundle path (including a name under assets/ that isn't an image or font), the document or lockfile, assets/remote/, or invalid JSON | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
412 | baseRevision is stale. The body is {error:"conflict", message, revision} with the draft's current revision. | |
413 | The file is over 16 MiB, or the bundle would be over 64 MiB (too-large) | |
415 | An asset's bytes aren't the image or font its name says (unsupported-media-type) | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
DELETE /v1/templates/{id}/draft/files/{path}
Remove one bundle file from the draft.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
path | path | string | yes | Bundle path, e.g. samples/invoice.json or assets/logo.svg. May contain slashes. |
environment | query | string | ||
baseRevision | query | integer (int64) |
Responses
| Status | Description | Body |
|---|---|---|
200 | Removed, with the new revision | |
404 | No such file, or the template is in the trash (in-trash, see InTrash) | |
412 | baseRevision is stale. The body is {error:"conflict", message, revision} with the draft's current revision. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/templates/{id}/outline
The engine's outline of the template (page templates and body nodes, with labels and locks).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
environment | query | string | ||
draft | query | boolean | Read the draft (or the published copy when no draft is written yet). |
Responses
| Status | Description | Body |
|---|---|---|
200 | {pages, body, diagnostics}, as the engine's POST /v1/outline | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/validate
Validate the draft (or a document sent in the body) against its bundle.
RFML §3 steps 1–3, done by the engine: structure, references and expressions against the dataset schemas. Problems are diagnostics (RFML §22.1), not errors.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body, optional (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
environment | string | ||
document | Document |
Responses
| Status | Description | Body |
|---|---|---|
200 | ||
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/publish
Publish the draft.
The publish pipeline: inspect → resolve (data sources, and library imports, subreports and library: assets by their version ranges, and those the resolved library reports and imported components use in turn, to 5 reports deep; cycles are ref/cycle, deeper nesting security/limit) → remote assets (fetched under assets.remote.<environment>: allowlisted origins, the SSRF guard, size caps; stored under assets/remote/) → validate (any error blocks, 422 with diagnostics and the stage; under outputs.pdf.standard PDF/UA-1 or PDF/A-2a+UA-1 the engine's errors include a11y/missing-title, a11y/missing-lang, a11y/untagged-pdf and a11y/missing-alt) → canonical form → rfml.lock.json (pinning sources, every library release resolved, nested ones included, and assets: the bundle's own assets/ files the document uses and library and remote assets, each by sha256) → tests (tests.publishGate: block, the default, refuses a publish whose tests fail or whose snapshots differ from the approved ones with 422 tests-failed; warn reports them in tests; off skips) → one commit on the environment branch → push when sourceControl.pushOnPublish. dryRun runs every check and returns the lockfile without writing.
Agents are held to agents.publish: deny (403), allow, or approval (the default: 202 with a pending approval, decided in /v1/approvals). Under approval the checks run first, as a dry run: a draft they block gets the 422 straight away instead of a pending approval. Dry runs need no approval.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body, optional (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
environment | string | ||
message | string | ||
dryRun | boolean | ||
baseRevision | integer (int64) | Publish only if the draft is still at this revision (409 otherwise). |
Responses
| Status | Description | Body |
|---|---|---|
200 | The revision (id is the commit) plus dryRun, draftRevision, diagnostics (warnings), lock, files, pushed, and tests (the gate's run, as POST …/tests/run returns it) when tests ran | |
202 | An agent's request is waiting for approval | |
403 | No template.publish, or agents.publish is deny | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
409 | The draft moved on from baseRevision, or the branch diverged | |
422 | Blocked: {error: "invalid", message, stage: inspect|resolve|assets|validate, diagnostics}, or {error: "tests-failed", message, stage: "tests", tests}. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/templates/{id}/history
Commits that touched the template on an environment branch.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
environment | query | string |
Responses
| Status | Description | Body |
|---|---|---|
200 | History | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/promote
Copy the template, with its lockfile, from one environment onto the next.
Promotes with dependencies: the library releases and data source versions the lockfile pins are copied to the target first when it lacks them. Everything is checked before anything is written; the promote is blocked with 409 dependencies when a pinned release or version isn't in the source environment as pinned, the target holds a different copy of it, or a source's connection has no binding in the target: {error: "dependencies", message, dependencies: [{kind: library|source, name, version, refs, problem}]}. Agents are held to agents.publish, as for publish.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body, optional (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
from | string | ||
to | string | Defaults to the environment after from. |
Responses
| Status | Description | Body |
|---|---|---|
200 | The promotion commit (a revision) plus dependencies: [{kind, name, version, refs, action: present|promote}] | |
202 | An agent's request is waiting for approval | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
409 | The target environment can't get what the template pins | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/rollback
Restore an older commit of this template as a new commit.
Agents are held to agents.publish, as for publish.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | Rollback commit | |
202 | An agent's request is waiting for approval | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/approvals
Agents' publish, promote and rollback requests that the caller could decide.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
status | query | string | pending (default), approved, rejected, withdrawn (the template went to the trash) or all |
Responses
| Status | Description | Body |
|---|---|---|
200 | {approvals: [{id, action, templateId, environment, from, commit, message, draftRevision, requestedBy, onBehalfOf, requestedAt, status, decidedBy, decidedAt, comment, result}]} | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/approvals/{approvalId}
Approve or reject an agent's request.
Only a person (not an agent) holding the action's permission where it lands may decide. Approving runs the action at once, attributed to the agent; a publish fails with 409 if the draft has changed since the request.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
approvalId | path | string | yes |
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
decision | approve, reject | yes | |
comment | string |
Responses
| Status | Description | Body |
|---|---|---|
200 | ||
403 | An agent, or no permission | |
409 | Already decided, or the draft moved on | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/tests/run
Run the template's embedded tests (RFML §20).
Runs the tests through the engine (/v1/test, with snapshots) on the draft, or the published copy with draft false. The engine is the judge: it compares each test's page snapshots (sha256 of the page SVG) with the approved baseline the bundle carries at tests/<test-slug>.snapshot.json (status same, changed or new), and a test with snapshots passes only when they are all the same. The clock is context.now, else 2000-01-01T00:00:00Z; a bundle's tests/context.json overrides both. rasters adds each snapshot page as a base64 PNG, for people to look at. Needs template.read.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes |
Request body, optional (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
environment | string | ||
draft | boolean | Defaults to true. | |
tests | array of string | Only these tests. | |
context | object | Render context {now, locale, timezone}. | |
rasters | boolean | Return snapshot pages as PNGs too. |
Responses
| Status | Description | Body |
|---|---|---|
200 | {id, environment, draft, revision, commit, passed, total, failed, results: [{name, passed, pages, expectations, diagnostics, snapshots: [{page, sha256, status, baseline?, png?}], approval?: {path, status, baseline}, failures}]}, as the engine's /v1/test reports them plus failures (why a test failed, one line each) | |
404 | The template is in the trash: {error: "in-trash", message, item}. item, the trash item's id, is there only for callers with template.read. | object |
409 | A published copy pins library releases its environment lacks (unresolved-library) | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/templates/{id}/tests/{name}/approve
Approve a test's page snapshots as its baseline.
Runs the one test on the draft and writes the baseline the engine proposes (approval.baseline, as JSON) at approval.path, tests/<test-slug>.snapshot.json, as a draft edit with a revision check; the next publish carries it. When the snapshots are already the same as the baseline nothing is written. Needs template.write.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | yes | |
name | path | string | yes | The test's name, URL-encoded |
Request body, optional (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
environment | string | ||
baseRevision | integer (int64) | ||
context | object | ||
message | string |
Responses
| Status | Description | Body |
|---|---|---|
200 | {revision, test, approval: {path, status, baseline}, snapshots} | |
404 | The document has no test of that name, or the template is in the trash (in-trash, see InTrash) | |
409 | The test takes no snapshots (no-snapshots) | |
412 | baseRevision is stale. The body is {error:"conflict", message, revision} with the draft's current revision. | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |