REST & gRPC API Specification (Draft)

This document defines the external API contract of the BPM Engine. The APIs are designed for service usage, external workers, and operational tooling.

Normative, testable contract (E2-4): docs/openapi.yaml is the machine-readable OpenAPI 3.1 contract of the REST API — paths, request/response schemas, security scheme, and the stable ErrorResponse.code enum. The CI consistency suite (crates/server/rest/tests/openapi_contract.rs) verifies the spec against the implementation in both directions (route parity, error-code set equality, sample-response validation). This document remains the prose specification; where the two disagree, openapi.yaml wins and a PR must fix the divergence.

Machine capability discovery (E4-1): GET /capabilities serves a derived, committed tool catalog (one tool per operation, parameter schemas, read-only/mutating classification, error examples) for AI agents — unauthenticated, like /health and /ready. See docs/agent-tools.md.


1. Design Principles


2. Core Resources

Resource Description
ProcessDefinition Deployed workflow definition
ProcessInstance Running workflow instance
Token Execution unit (internal)
Task Executable node (user / external)
Event Engine event (outbox)

3. REST API

Base URL:

/api/v1

3.1 Process Definition

Deploy a Process

POST /process-definitions

Request:

{
  "id": "order_process",
  "version": "1.0.0",
  "definition": { /* DSL / BPMN JSON */ }
}

Response:

{
  "id": "order_process",
  "version": "1.0.0",
  "status": "DEPLOYED"
}

3.2 Process Instance

Start a Process Instance

POST /process-instances

Request:

{
  "process_def_id": "order_process",
  "variables": {
    "order_id": "123"
  }
}

Response:

{
  "instance_id": "pi_abc123",
  "status": "RUNNING"
}

Get Instance State

GET /process-instances/{instance_id}

Response:

{
  "instance_id": "pi_abc123",
  "status": "RUNNING",
  "current_nodes": ["task_a", "task_b"]
}

Get Instance Execution History

GET /process-instances/{instance_id}/history

Query (optional):

Response:

{
  "instance_id": "pi_abc123",
  "events": [
    {
      "sequence": 0,
      "id": "event-uuid",
      "event_type": "ProcessStarted",
      "category": "instance",
      "occurred_at": "1738454400",
      "payload": { "instance_id": "pi_abc123", "process_id": "order_process", ... }
    },
    {
      "sequence": 1,
      "id": "event-uuid-2",
      "event_type": "TokenArrived",
      "category": "token",
      "occurred_at": "1738454401",
      "payload": { "instance_id": "pi_abc123", "token_id": "...", "node_id": "task_a" }
    }
  ]
}

History API Semantics


Get Instance Trace (aggregated view)

GET /process-instances/{instance_id}/trace

Returns instance state plus token timelines and external-task history aggregated by token/task. Use for a high-level view; use /history for the raw event timeline with sequence and category. Trace and History use the same event source; Trace is an aggregated view and shares the same semantics as History.


Get Instance Steps (unified step timeline, E6-1)

GET /process-instances/{instance_id}/steps

One step per (node_id, token_id) in causal order, joining token transitions, worker lease attempts (lock → complete/fail, a re-lock after lease expiry closes the open attempt as expired) and the E3-1 AI task envelope metadata (model/tool versions, cost), keyed by the trace id (trace_id = the instance id in v1). Read-only aggregation over the instance, definition and history stores — no new stores, no new event types; sensitive-by-default: the response carries no variable values (only event types/timestamps, worker ids, retry counters, variable name lists, and envelope metadata summaries; redacted or unparsable envelopes degrade to {"opaque": true}). Full contract: docs/openapi.yaml (StepsResponse); semantics and design decisions: docs/trace.md.


3.3 Tasks

List Pending Tasks

GET /tasks?type=user|external

Response:

[
  {
    "task_id": "task_1",
    "node_id": "approve_order",
    "instance_id": "pi_abc123",
    "type": "user"
  }
]

Complete Task

POST /tasks/{task_id}/complete

Request:

{
  "variables": {
    "approved": true
  }
}

Response:

{
  "status": "COMPLETED"
}

3.4 Signals & Messages

Send Signal

POST /signals

Request:

{
  "signal": "payment_received",
  "instance_id": "pi_abc123",
  "payload": {}
}

3.5 Operations

Cancel Instance

POST /process-instances/{id}/cancel

Retry Token

POST /tokens/{token_id}/retry

4. gRPC API (External Worker)

4.1 Design Goal


4.2 Service Definition

service ExternalTaskService {
  rpc FetchTasks(FetchRequest) returns (stream Task);
  rpc CompleteTask(CompleteRequest) returns (CompleteResponse);
  rpc FailTask(FailRequest) returns (FailResponse);
}

4.3 Fetch Tasks

message FetchRequest {
  string worker_id = 1;
  repeated string task_types = 2;
  int32 max_tasks = 3;
}

4.4 Complete Task

message CompleteRequest {
  string task_id = 1;
  map<string, string> variables = 2;
}

4.5 Fail Task

message FailRequest {
  string task_id = 1;
  string reason = 2;
  int32 retry_after_seconds = 3;
}

5. Idempotency & Safety


5a. Error Contract (E2-4)

Every error response uses the ErrorResponse body:

{
  "error": "human-readable message (wording is NOT a contract)",
  "code": "stable_snake_case",
  "invariant_violation": "OptionalKindName"
}

Stable error codes (single source of truth: crates/server/rest/src/routes.rs::error_codes::ERROR_CODES, mirrored in docs/openapi.yaml — the CI suite asserts set equality; renaming a code is a breaking change):

Code HTTP Meaning
not_found 404 Unknown instance / definition / task / dead-letter / replay-session id (and, under JWT auth, an id of ANOTHER tenant — E5-1)
unauthorized 401 Missing or invalid API key
invalid_token 401 Missing/malformed/rejected JWT when JWT auth is configured (E5-1)
forbidden 403 RBAC: the verified role lacks permission, or the X-Tenant-Id header conflicts with the token’s tenant claim (E5-1)
rate_limited 429 Request rate above the configured window
invalid_request 400 Generic client error (malformed path ids, bad query params, invalid idempotency key)
invalid_json 400/422 Request body is not valid JSON for the endpoint (syntax/content-type → 400; type mismatch → 422)
internal 500 Unexpected server-side failure
deploy_failed 500 Definition compiled but could not be persisted
invalid_format 400 validate/dry-run format is neither bpmn nor dsl (E2-2)
empty_source 400 validate/dry-run source is empty (E2-2)
invalid_request_body 400 validate/dry-run body is not deserializable JSON (E2-2)
variable_too_large 400 Variable value exceeds engine.variable_max_bytes (E2-3)
variable_invalid_envelope 400 Malformed/unsupported envelope version (E2-3)
variable_invalid_artifact_uri 400 Artifact URI violates the E2-3 URI contract
sensitive_requires_encryption 400 Sensitive variable not present / not encrypted (E2-3)
encryption_key_not_configured 400 encrypted requested while BPM_VARIABLE_ENCRYPTION_KEY is unset
task_envelope_invalid 400 AI task envelope failed validation (E3-1, sub-code in message)
reserved_variable_key 400 User-supplied __task_envelope variable (E3-1)
invalid_retry_category 400 fail category is not transient/permanent (E3-1; the category is behavioral since E3-2: permanent skips the retry loop — straight to Failed + DLQ)
missing_required_fields 400 user-task complete without the form’s required fields
task_not_found 400 External-task store: unknown task (kept 400 for compat)
task_not_locked 400 External-task store: task not in the Locked state
lock_expired 400 External-task store: lease expired
invariant_violation 400 Engine invariant violated (kind in invariant_violation + header)
lock_conflict 409 Lock extension refused (unknown / not locked / other owner)

Backward compatibility: pre-E2-4 error bodies only GAIN the code field; existing codes (E2-2/E2-3/E3-1) are unchanged. The deploy endpoint’s 400 parse ({"error": ...}) and compile ({"errors": [...]}) shapes are legacy-stable (no code field); its persistence failure moved from the parse shape to 500 {error, code: "deploy_failed"} (E1-1 review fix).


5b. Pagination (E2-4)


5c. Idempotency Keys — start-instance (E2-4)

POST /api/v1/process-instances accepts an optional Idempotency-Key HTTP header (1–128 printable ASCII bytes; malformed keys are rejected with 400 invalid_request rather than silently ignored).

Because the (key, tenant_id) pair co-forms the idempotency primary key, the co-stored X-Tenant-Id is bounded at this save boundary: when an Idempotency-Key is supplied, the tenant id must be at most 128 printable ASCII bytes (400 invalid_request “tenant id too long” otherwise). X-Tenant-Id keeps its unbounded, pass-through semantics on every other endpoint and on key-less starts.

Semantics:

Atomicity boundary (honest, single-service): the check-then-create sequence is not cross-process atomic — the engine runs as one service per database (the same boundary as the migration runner), so two concurrent identical requests can still race. A key-recording failure AFTER the instance was created is reported as 500 internal with an explicit message (the retry may then create a second instance). Cross-process uniqueness would require a unique-constraint insert path; out of scope for v1.


6. Authentication & Authorization (Future)


7. Versioning Strategy

Versioning rules (E2-4, binding for /api/v1)


8. API & Semantic Stability

API Stability Policy


From v0.1.0 onward, the following API and semantics are committed to no breaking changes (backward-compatible):

Stable (v0.1.0)

May evolve


9. History & Trace Semantic Guarantees

This section defines the semantic contract of the History and Trace APIs. These guarantees are intentional design commitments and should be considered stable once released.

History Events

The History API exposes a sequence of immutable execution events emitted by the engine.

Semantic guarantees:

Trace API

The Trace API provides a derived, read-only view over the underlying history events.

Semantic guarantees:

Stability & Compatibility

Breaking changes to history semantics require a major version change.

Non-Goals

The History and Trace APIs are not intended to:

They are correctness and auditability primitives.


10. Relationship to Other Docs


A stable API turns an engine into a platform.