Engine API

Every REST endpoint the engine exposes, with request and response shapes: register/update/delete Steps, start and query Flows, preview execution plans, check step health, and post async webhook completions.

The engine listens on port 8080 by default. All paths below assume http://localhost:8080.

Engine

GET /engine

Returns the combined catalog and cluster snapshot.

{
  "last_updated": "2026-05-09T17:00:00Z",
  "steps": {
    "lookup-customer": { "...full step definition..." }
  },
  "attributes": {
    "customer_id": {
      "providers": [],
      "consumers": ["lookup-customer"]
    },
    "customer": {
      "providers": ["lookup-customer"],
      "consumers": []
    }
  },
  "health": {
    "argyll-1": {
      "last_seen": "2026-05-09T17:00:00Z",
      "health": {
        "lookup-customer": { "status": "healthy" }
      }
    }
  }
}

The attributes map is the engine-level dependency graph: each attribute name lists which Steps produce it and which consume it. The health map is keyed by node ID; each node has last_seen and a per-step health map. HealthState.status is one of healthy, unhealthy, or unknown, with an optional error field for unhealthy states. Use GET /engine/catalog and GET /engine/cluster to fetch catalog and cluster state individually.

GET /engine/catalog

Returns the current catalog state: all registered steps and the engine-level attribute dependency graph.

{
  "last_updated": "2026-05-09T17:00:00Z",
  "steps": {
    "lookup-customer": { "...full step definition..." }
  },
  "attributes": {
    "customer_id": { "providers": [], "consumers": ["lookup-customer"] },
    "customer":    { "providers": ["lookup-customer"], "consumers": [] }
  }
}

GET /engine/catalog/events

Returns all raw events for the catalog aggregate (step registrations, updates, and unregistrations) from sequence zero to the present. Useful for auditing step registration history.

{
  "events": [
    {
      "sequence": 0,
      "timestamp": "2026-05-09T17:00:00Z",
      "type": "step_registered",
      "aggregate_id": ["catalog"],
      "data": { "...step_registered payload..." }
    }
  ],
  "count": 1
}

GET /engine/cluster

Returns the current cluster state: per-node step health. Configured Raft nodes are always present even if no health events have been received for them.

{
  "last_updated": "2026-05-09T17:00:00Z",
  "nodes": {
    "argyll-1": {
      "last_seen": "2026-05-09T17:00:00Z",
      "health": {
        "lookup-customer": { "status": "healthy" }
      }
    }
  }
}

GET /engine/cluster/events

Returns all raw events for the cluster aggregate (step health changes) from sequence zero to the present.

{
  "events": [
    {
      "sequence": 0,
      "timestamp": "2026-05-09T17:00:00Z",
      "type": "step_health_changed",
      "aggregate_id": ["cluster"],
      "data": { "...step_health_changed payload..." }
    }
  ],
  "count": 1
}

GET /health

Liveness check, returns 200. Response includes X-Argyll-Raft-State header (leader, candidate, follower, or unknown) for leader-aware load balancers.

Steps

POST /engine/step

Register a Step.

{
  "id": "lookup-customer",
  "name": "Lookup Customer",
  "type": "sync",
  "http": {
    "method": "GET",
    "endpoint": "https://api.example.com/customers/{customer_id}",
    "timeout": 5000
  },
  "attributes": {
    "customer_id": { "role": "required", "type": "string" },
    "customer": { "role": "output", "type": "object" }
  }
}

GET /engine/step

List all registered steps.

{
  "steps": [
    { "id": "lookup-customer", "name": "Lookup Customer", "...": "..." },
    { "id": "send-confirmation", "name": "Send Confirmation", "...": "..." }
  ],
  "count": 2
}

GET /engine/step/{stepId}

Get a specific Step definition. Returns the full Step registration object.

PUT /engine/step/{stepId}

Replace a Step definition with a new version. Body shape matches POST /engine/step.

DELETE /engine/step/{stepId}

Unregister a Step.

Step Health

GET /engine/health

Aggregated Step health across the cluster (a ClusterState).

{
  "last_updated": "2026-05-09T17:00:00Z",
  "nodes": {
    "argyll-1": {
      "last_seen": "2026-05-09T17:00:00Z",
      "health": {
        "lookup-customer": { "status": "healthy" },
        "send-confirmation": { "status": "unhealthy", "error": "connection refused" }
      }
    }
  }
}

GET /engine/health/{stepId}

Resolved health for one Step merged across nodes.

{ "status": "healthy" }

Returns 404 if no health record exists for the Step.

Flows

POST /engine/flow

Start a Flow.

{
  "id": "wf-123",
  "goals": ["send-confirmation"],
  "init": {
    "customer_id": ["cust-456"],
    "order_amount": [99.99]
  },
  "labels": { "customer": "cust-456" }
}

init values are arrays per Attribute, this disambiguates scalar vs. array values. labels is optional.

Response:

{
  "message": "",
  "flow_id": "wf-123"
}

The Flow runs asynchronously. Poll GET /engine/flow/{flowId} or subscribe via WebSocket to track progress.

POST /engine/plan

Preview an Execution Plan without starting a Flow. Same request body as starting a Flow (the id field is ignored).

{
  "goals": ["send-confirmation"],
  "init": {
    "customer_id": ["cust-456"]
  }
}

Response is the full ExecutionPlan:

{
  "goals": ["send-confirmation"],
  "required": ["customer_id", "order_amount"],
  "steps": {
    "lookup-customer": { "...": "..." },
    "validate-payment": { "...": "..." },
    "send-confirmation": { "...": "..." }
  },
  "attributes": {
    "customer_id": { "providers": [], "consumers": ["lookup-customer"] },
    "customer":    { "providers": ["lookup-customer"], "consumers": ["validate-payment", "send-confirmation"] },
    "valid":       { "providers": ["validate-payment"], "consumers": ["send-confirmation"] }
  },
  "children": {},
  "excluded": {
    "satisfied": {},
    "blocked": {},
    "missing": {}
  }
}
FieldMeaning
goalsGoal Step IDs the Plan was built for
requiredAttribute names that must be supplied in init to satisfy the Plan
stepsStep definitions snapshotted at Plan creation time
childrenFor Sub-Flow Steps, their precomputed child Execution Plans
attributesPer-Attribute provider/consumer graph for this Plan
excludedSteps reachable from the Goals that were excluded, with the reason

The excluded block has three sub-maps:

  • satisfied: Step → output names already in init, so the Step is unnecessary
  • blocked: Step → input names whose init values prevent collection from ever satisfying (for example a none policy with an init value present)
  • missing: Step → required input names that could not be satisfied by any provider or init value

GET /engine/flow

List recent flows. Active flows appear first, then completed and failed flows, all sorted most-recent first within each group. Each item includes id, status, and timestamp.

List and query statuses change only when a Flow is deactivated: a Flow remains active in these results even if its full state already reports terminal completed or failed while work can still produce side effects (changes to external systems) or run compensation. For completed or failed results, timestamp is the deactivation time.

[
  { "id": "wf-123", "status": "completed", "timestamp": "2026-05-09T17:00:02Z" },
  { "id": "wf-122", "status": "failed",    "timestamp": "2026-05-09T16:55:00Z" }
]

POST /engine/flow/query

Query flows with ID prefix, labels, deactivation-based status filters, and pagination.

Request:

{
  "id_prefix": "wf-",
  "labels": { "customer": "cust-456" },
  "statuses": ["active", "failed"],
  "limit": 50,
  "cursor": "",
  "sort": "recent_desc"
}

All fields are optional. sort accepts recent_desc (most-recent first) or recent_asc (oldest first, default for query). Note: GET /engine/flow always returns recent_desc.

Response:

{
  "flows": [
    {
      "id": "wf-123",
      "status": "active",
      "timestamp": "2026-05-09T17:00:00Z"
    }
  ],
  "count": 1,
  "total": 120,
  "has_more": true,
  "next_cursor": "opaque-cursor-string"
}

Pass next_cursor back as cursor to fetch the next page.

GET /engine/flow/{flowId}

Get full Flow state. This response reports terminal completed or failed status as soon as callers can use the outcome; deactivated_at remains unset while pending or active work may still produce side effects, or compensation may still run.

{
  "id": "wf-123",
  "status": "completed",
  "created_at": "2026-05-09T17:00:00Z",
  "completed_at": "2026-05-09T17:00:02Z",
  "deactivated_at": "2026-05-09T17:00:02Z",
  "last_updated": "2026-05-09T17:00:02Z",
  "plan": { "...ExecutionPlan as above..." },
  "labels": { "customer": "cust-456" },
  "metadata": { "source": "checkout" },
  "attributes": {
    "customer_id": [
      { "value": "cust-456", "set_at": "2026-05-09T17:00:00Z" }
    ],
    "customer": [
      {
        "value": { "id": "cust-456", "name": "Alice" },
        "step": "lookup-customer",
        "set_at": "2026-05-09T17:00:01Z"
      }
    ]
  },
  "executions": {
    "lookup-customer": {
      "status": "completed",
      "started_at": "2026-05-09T17:00:00Z",
      "completed_at": "2026-05-09T17:00:01Z",
      "duration": 1000,
      "inputs": { "customer_id": "cust-456" },
      "outputs": { "customer": { "id": "cust-456", "name": "Alice" } }
    }
  }
}

Each entry in attributes is an array of AttributeValue objects (value, step, set_at). Multiple values can accumulate for the same Attribute name when several upstream Steps produce it; the consumer’s collect policy decides how they’re combined. step is omitted for values from Flow init.

executions maps Step ID to its execution record (status, started_at, completed_at, duration, inputs, outputs, error, plus work_items for Steps with for_each). Failed and skipped steps also include unsatisfied: an array of input names that could not be satisfied.

GET /engine/flow/{flowId}/status

Lightweight deactivation-based status check. This reports active until deactivation; use the full Flow state endpoint or terminal flow events when callers must use the outcome before remaining work has finished.

{
  "id": "wf-123",
  "status": "completed"
}

GET /engine/flow/{flowId}/events

Returns all raw events for the flow aggregate from sequence zero to the present. Returns 404 if the flow ID is not found.

{
  "events": [
    {
      "sequence": 0,
      "timestamp": "2026-05-09T17:00:00Z",
      "type": "flow_started",
      "aggregate_id": ["flow", "wf-123"],
      "data": { "...flow_started payload..." }
    },
    {
      "sequence": 1,
      "timestamp": "2026-05-09T17:00:01Z",
      "type": "step_started",
      "aggregate_id": ["flow", "wf-123"],
      "data": { "...step_started payload..." }
    }
  ],
  "count": 2
}

Each event has sequence (monotonically increasing within the aggregate), timestamp, type, aggregate_id, and data (the type-specific payload). See the WebSocket reference for the full list of event types.

Webhook

POST /webhook/{flowId}/{stepId}/{token}

Async Step completion. POST output Attributes on success or application/problem+json on failure. The engine deduplicates by token. Duplicate completions for the same token are ignored and return 200, so retrying a webhook call is safe.

WebSocket

GET /engine/ws

Real-time event stream. See WebSocket API.