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.

Caller
Any Caller
app · agent · LLM
POST /commands
→
BEST Endpoint
Validates & Queues
201 Accepted (async)
Async processing
→
Service
Domain Handler
processes · emits event

POST /commands is a behaviour endpoint, not a resource collection. Sending a command is not creating a "command resource" — it is expressing an intent. /commands is a single entry point for every operation the service accepts; what happens is determined entirely by the envelope type, 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:

  1. Validate required envelope attributes.
  2. Look up the schema in the server's own catalogue, keyed by type.
  3. Validate data against that schema.
  4. 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-defined code that names the condition, a message that says what to do, and details a program can act on.
  5. Valid → durably queue and return 201 Created with { "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 dataschema field is informational metadata, never an instruction. Servers must not fetch a caller-supplied dataschema URI (SSRF — see Security).
  • The envelope id is 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 same id from the same credential) is rejected with 409 naming the id, or safely ignored; a reused id with a different payload returns 409. A duplicate that is safely ignored is answered as the original was, with the original's status, id and correlationId, 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 no correlationid takes its id as its correlation, which belongs to the sender that used it first (Command Results and Correlation).
  • type is the routing key. source must not be the sole routing key.
  • 201, not 202: 201 signals the command was durably recorded and processing will happen. Use 202 only 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.

Why results are retrieved this way

See Design Decisions — Command Result Retrieval.

spec/v0.9.21