Skip to content

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.