Commands — io.best.agents.commands
Commands are sent to the service by any caller — a Process Manager, an AI agent, a UI, or another service.
POST /commandsis a behaviour endpoint, not a resource collection. Sending a command is not creating a "command resource" — it is expressing an intent./commandsis a single entry point for every operation the service accepts; what happens is determined entirely by the envelopetype, not the HTTP verb or the URL. In REST you manipulate resources; in BEST you invoke named operations and observe the facts they produce.
Commands ride the CloudEvents 1.0 envelope (cloudEvent.json) — see SPEC.md — Wire Format for the field table and examples.
Commands are intents to change a domain service. The service validates, queues, and processes them asynchronously; results surface as events.
Schema: commands.json
| Method | Path | Description |
|---|---|---|
| GET | /commands |
Command catalogue — all accepted command types with schema URIs |
| POST | /commands |
Send a command (BEST envelope). Validates, queues, returns 201. |
| GET | /commands/{schema}/{version} |
JSON Schema document for one command type/version (application/schema+json) |
Command Catalogue
{
"commands": [
{
"schema": "propose-counter",
"version": "1.0",
"commandType": "ProposeCounter",
"dataschema": "https://api.example.com/commands/propose-counter/1.0",
"description": "Propose a counter-offer in a contract negotiation"
}
]
}
| Field | Required | Description |
|---|---|---|
schema |
yes | Command schema name in kebab-case — the {schema} path segment. Distinct from the envelope type, which commandType states. |
version |
yes | Schema version (1.0, 2.1) — first-class, no URI parsing needed |
commandType |
no | The exact envelope type to send for this command (ProposeCounter). When absent, consumers derive it as the PascalCase form of schema; servers should state it, because a derivation is a guess. Like workflows and impact it is carried on both surfaces — the catalogue entry and the schema document, as a top-level member with the same value. (It cannot be called type there: JSON Schema owns that keyword.) |
dataschema |
yes | Resolvable URI to the JSON Schema for data — the canonical value for a command's dataschema field. Resolves to GET /commands/{schema}/{version} on this same surface. |
description |
no | What the command does |
workflows |
no | Ids of published workflows this command participates in — servers should populate it for every operation that appears in a recipe |
impact |
no | High-impact annotation — declares that the command is high-impact and that a human-facing consumer must warn and confirm before submitting |
Impact Annotations
Some commands do not share a blast radius with the rest of the catalogue: the person the consumer acts for would want to be asked first. They move money, destroy data, cannot be undone — or commit the person to something, or let someone new into what is theirs. The Security Requirements already oblige servers to place extra controls on such commands — but until a client can discover which commands those are, only the server side of the obligation is implementable. The optional impact annotation closes that gap:
{
"schema": "submit-order",
"version": "1.0",
"dataschema": "https://api.example.com/commands/submit-order/1.0",
"description": "Submit a market order to the caller's broker connection",
"impact": {
"categories": ["financial"],
"confirmation": "required",
"warning": "Places a real order on your broker account. Capital is at risk."
}
}
The annotation is not about money. A command that commits the person carries it just the same:
{
"schema": "open-account",
"version": "1.0",
"dataschema": "https://api.example.com/commands/open-account/1.0",
"description": "Open an account for the person the caller acts for",
"impact": {
"categories": ["commitment"],
"confirmation": "required",
"warning": "Opens an account in your name under the terms at example.com/terms. The free plan applies until you choose another."
}
}
| Field | Required | Description |
|---|---|---|
categories |
yes | What kind of impact — named values: financial (moves money or puts capital at risk), destructive (removes data or state), irreversible (no compensating command), compliance (bypasses or alters a compliance control), commitment (binds the person: opens an account, accepts terms, starts a subscription, creates personal data in their name), access (grants a principal access to what belongs to the person: issues a credential, shares a resource, invites a member). The vocabulary is open; consumers treat unknown values as high-impact. |
confirmation |
yes | required: a consumer acting on behalf of a human must not submit the command without explicit, per-submission confirmation from that human. recommended: the consumer should confirm but may proceed where the human has durably authorized this class of operation. |
warning |
no | Human-readable warning the consumer should surface to the human, substantially intact, before asking for confirmation. |
Like workflows, the annotation is carried on both surfaces that describe an operation — the catalogue entry and the schema document (GET /commands/{schema}/{version}) as a top-level member — and both must carry the same value.
The annotation is descriptive, not enforcement. It tells a well-behaved consumer what to do before submitting; it never replaces the server-side controls of Security Requirements — high-impact commands, because a server cannot rely on clients honoring it. Service-to-service automation with no human principal is governed by those server-side controls alone — the annotation does not require inventing a human to ask.
Ingestion Semantics
POST /commands processing:
- Validate required envelope attributes.
- Look up the schema in the server's own catalogue, keyed by
type. - Validate
dataagainst that schema. - Check the command against the service's current state: a well-formed command it cannot honour now (no room for it, nothing by the name it gives) is refused with
400, a service-definedcodethat names the condition, amessagethat says what to do, anddetailsa program can act on. - Valid → durably queue and return
201 Createdwith{ "id": "<command id>", "correlationId": "<correlation identifier>" }. Invalid →400.
A command refused at step 4 was never accepted: its id is free for the corrected retry. A retry of an accepted command is answered as the original was, without the check, where the service answers a duplicate rather than rejecting it (below).
- The inbound
dataschemafield is informational metadata, never an instruction. Servers must not fetch a caller-supplieddataschemaURI (SSRF — see Security). - The envelope
idis an idempotency key, scoped to the credential that sent it and kept for a retention window the service states in its documentation: a duplicate (the sameidfrom the same credential) is rejected with409naming theid, or safely ignored; a reusedidwith a different payload returns409. A duplicate that is safely ignored is answered as the original was, with the original's status,idandcorrelationId, and runs nothing again, so a client that lost the first answer and retries gets exactly what it missed. Two credentials do not collide on an idempotency key, though a command that names nocorrelationidtakes itsidas its correlation, which belongs to the sender that used it first (Command Results and Correlation). typeis the routing key.sourcemust not be the sole routing key.201, not202:201signals the command was durably recorded and processing will happen. Use202only if your implementation cannot durably enqueue before responding. A durably recorded command is processed even if the service restarts before it ran: after a restart the service processes it, through the same state check as if it had just arrived, or publishes its failure event. It is never dropped silently.
Command Results and Correlation
BEST defines no synchronous command response. The result of processing is one or more events, tied to the command by the first-class correlationid envelope attribute. How a client gets them is the implementor's decision: by polling a query the service defines, or, where it declares the events capability, from GET /events and its SSE stream:
POST /commands → 201 { "id": "abc123", "correlationId": "abc123" }
GET /queries/{schema}?… → a query the service defines
GET /events?correlationId=abc123 → what has already happened
GET /events/stream?correlationId=abc123 → what happens next (push)
The caller may set correlationid on the command; when omitted, the server adopts the command's id — either way the 201 response echoes the effective value as correlationId. Every event produced by processing the command must carry that identifier in its correlationid envelope attribute, so any consumer — including one that never saw the command — can match events to their originating submission. So does every later fact about what the command created (a deployment that ends hours after it was ready, a job that finishes), for as long as the thing lives: a consumer filtering on the correlation id sees its whole life. Multi-step processes propagate it: a follow-up command issued in reaction to an event, by the same sender, should carry the same correlationid, so one identifier ties the steps of a process together.
A correlation id belongs to the sender that first used it on a surface: the authenticated credential whose accepted command first carried it. A command from another sender naming it must be refused with a 400 whose details name the field, and what a credential may read of a correlation derives from that ownership, never from the identifier alone. Two consequences follow. A command with no correlationid takes its id as correlation, so two senders sending one id without a correlation are one accepted and one refused; the fix is a correlation id of the sender's own. And a chain of commands under one correlation id is one sender's: a service that reacts to an event under its own credential starts a correlation of its own.
The schema document at GET /commands/{schema}/{version} may declare a produces array of PascalCase event types the command can raise (e.g. ["CounterProposed", "NegotiationFailed"]). Failure outcomes are ordinary events in that list; naming conventions (*Failed) are service-defined. BEST defines no timeout protocol — services should document expected processing times and always publish a failure event rather than silently dropping a command; callers decide how long to wait. When the service publishes workflows, the document should also carry the operation's workflows cross-link array; when the catalogue entry carries an impact annotation, the document must carry the same annotation as a top-level impact member.