Distributed execution¶
Orchestra can drive a workflow that lives on another site. The trick is a single
contract (OrchestraClientInterface) with two implementations behind it:
one in-process, one over HTTP. A caller depends only on the interface, so the
same code runs same-site and cross-site; the container binds whichever half
the site provides.
flowchart LR
caller["Caller<br/>(ECA action, inbox,<br/>task handler)"] --> contract{{OrchestraClientInterface}}
contract -.->|same site| local[LocalOrchestraClient]
contract -.->|remote site| remote[RemoteOrchestraClient]
local --> engine[(Workflow engine)]
remote -->|OAuth2 + HTTP| api[orchestra_server_api]
api --> serverClient[LocalOrchestraClient] --> serverEngine[(Engine on the server)]
The contract¶
orchestra_api defines OrchestraClientInterface and ships
LocalOrchestraClient, which wraps the engine and entity storage in process.
The surface is small and transport-friendly: values cross as scalars and plain
arrays, never entity objects, so a remote half can rebuild them from JSON:
| Operation | Meaning |
|---|---|
startProcess(definition_id, variables) |
Start an instance; returns its ID. |
signalToken(token_id) |
Resume a parked token (the completion primitive). |
getInstance(instance_id) |
An instance with its tokens, or NULL. |
listInstances(filters) |
Instance summaries, newest first. |
getVariables(instance_id) |
The instance's variables as a name→value map. |
setVariable(instance_id, name, value) |
Create or replace one variable. |
Every operation is scoped to the current tenant: an instance, token or variable in another tenant is invisible and cannot be acted on.
The HTTP API¶
orchestra_server_api exposes the contract over HTTP under /orchestra-api,
guarded by OAuth2 (the client-credentials grant). Its controllers are a thin
transport: each delegates to a LocalOrchestraClient and serializes the result;
no workflow logic lives there.
| Method & path | Operation |
|---|---|
POST /orchestra-api/process/{definition_id}/start |
startProcess |
GET /orchestra-api/instances |
listInstances |
GET /orchestra-api/instance/{instance_id} |
getInstance |
POST /orchestra-api/token/{token_id}/signal |
signalToken |
GET /orchestra-api/instance/{instance_id}/variables |
getVariables |
PUT /orchestra-api/instance/{instance_id}/variable/{name} |
setVariable |
The full contract (request and response shapes, status codes, and the OAuth2
scheme) is described in OpenAPI 3.1 in
openapi.yaml, with a
rendered reference alongside it in
api-reference.html
(a ReDoc page that loads the spec).
Tenant binding¶
A consumer is bound to one tenant by an immutable orchestra_tenant field added
to its OAuth consumer. A tagged tenant resolver (ConsumerTenantResolver) reads
that binding on each request, so every API call a consumer makes is
automatically scoped to its tenant: the controller never resolves a tenant
itself, and a consumer cannot reach across tenants.
The remote client¶
orchestra_client ships RemoteOrchestraClient, which binds
OrchestraClientInterface to a remote server's HTTP API. It obtains a bearer
token via client-credentials (the secret held by the Key
module, never in config) and calls /orchestra-api. Enabling it rebinds the
contract to the remote adapter, so the same caller code now runs against the
remote site. The local and remote halves are symmetric: neither the caller nor
the server knows which side the other is on.