Skip to main content

OpenClaw Orchestration Runtime Design

Status: implemented M9 pilot design and rollout reference, extended through M10 (concrete executor, agent-node pilot, capability policy, and flip-readiness runbook).

Issue: #62

Decision

Bloom remains the product and coordination control plane. OpenClaw may become an execution runtime for selected workflow work, but it does not become the source of truth for Bloom's product state, GitHub coordination semantics, accounts, or business invariants.

This keeps the migration incremental: Bloom can route one workflow path through OpenClaw, observe it, and switch back to the built-in runtime without moving the whole SDLC process at once.

Responsibilities That Stay In Bloom

Bloom owns the product model and all user-visible SDLC semantics:

  • Product state: requirements, PRD revisions, milestones, tickets, reviews, specialist findings, lifecycle state, and project progress.
  • GitHub coordination semantics: issue and PR labels, assignees, reviewer requests, human and agent ownership signals, milestone completion, review gates, merge policy, and dependency-aware delegation.
  • Account and session state: dashboard sessions, OAuth identities, Telegram account links, active project routing, and project visibility.
  • Project read model: dashboard overviews, project details, SSE project events, status answers, and digest data.
  • Business invariants: approval gates, destructive action confirmations, owner opt-ins, review strictness, escalation rules, and rollback authority.
  • Inbound durability: webhook verification, fast acknowledgment, durable job enqueue, retry policy, dead-letter behavior, and event-key dedupe.

In short: Bloom decides what should happen and what state changed.

Responsibilities That May Move To OpenClaw

OpenClaw may own bounded execution responsibilities behind an adapter:

  • Agent turn dispatch.
  • Tool execution for selected workflow nodes.
  • OpenClaw session routing.
  • Runtime-level retries inside one Bloom job attempt.
  • Async task tracking for delegated node runs.
  • Provider and tool routing for node execution.

In short: OpenClaw may execute work that Bloom requested, then report the result back to Bloom.

Adapter Boundary

The new seam should sit above the current built-in workflow engine, not inside the webhook routes or GitHub handlers.

Implemented seam:

class OrchestrationRuntime(Protocol):
@property
def name(self) -> str: ...

async def start(self, thread_id: str, seed: dict[str, Any]) -> RunState: ...
async def advance(self, run: RunState) -> RunState: ...
async def resume(self, run: RunState, resume_input: Any = None) -> RunState: ...
def observe(self, run: RunState) -> RuntimeObservation: ...

Initial implementations:

  • BuiltInOrchestrationRuntime: wraps today's Engine(spec, WorkflowContext) behavior and remains the default.
  • OpenClawPilotRuntime: disabled-by-default adapter that keeps Bloom's built-in engine responsible for graph traversal and reducers, but routes allowlisted low-risk nodes through an OpenClaw execution adapter while preserving Bloom's RunState as the persisted state contract.

The existing Orchestrator continues to own entry points such as user messages, GitHub PR events, issue events, and project ticks. It calls the configured runtime to start, advance, or resume workflow state.

Idempotency

Bloom keeps event_key as the source of truth for inbound event dedupe. The current fence in RunState.processed_keys stays Bloom-owned and is saved with the run state.

Before invoking OpenClaw, Bloom should persist a correlation record keyed by:

thread_id + event_key + current_node + attempt

The correlation record should include:

  • Bloom thread id.
  • Bloom job id or source event key.
  • Workflow node id.
  • Runtime provider.
  • OpenClaw session key or execution id.
  • Status: pending, running, succeeded, failed, abandoned.
  • Safe error metadata when failed.

Duplicate delivery must find the existing correlation and observe or resume it, not create another OpenClaw execution.

The implementation stores these records through RuntimeCorrelationStore, with InMemoryRuntimeCorrelationStore for development/tests and SupabaseRuntimeCorrelationStore for the production Postgres backend.

M9 Pilot Runtime

Issue #66 introduces the first OpenClaw-backed workflow execution path:

  • Runtime flag: BLOOM_ORCHESTRATION_RUNTIME=openclaw_pilot.
  • Allowlist: BLOOM_OPENCLAW_ORCHESTRATION_PILOT_NODES=track_progress.
  • Failure mode: BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fallback or fail_closed.

The chosen node is track_progress, a read-only GitHub status refresh that writes only Bloom's progress field. All other node types and node ids continue through the built-in WorkflowContext bindings.

The pilot adapter creates a stable OpenClaw session key per Bloom thread and node, reserves a runtime correlation record before execution, records success or failure, and reuses cached successful results for duplicate execution inputs. If OpenClaw execution fails and failure mode is fallback, Bloom runs the built-in node handler and logs a secret-safe diagnostic. If failure mode is fail_closed, the runtime raises an operator-visible error instead of silently continuing.

Production defaults remain BLOOM_ORCHESTRATION_RUNTIME=builtin, so existing deployments do not change behavior until the pilot flag is enabled.

M10-1 Concrete Executor Contract

Issue #82 defines the concrete OpenClaw runtime execution contract that a real OpenClawNodeExecutor must implement, and adds the minimal config surface for it. Issue #83 implements that contract as OpenClawGatewayNodeExecutor/OpenClawExecutorGatewayClient (bloom/workflow/openclaw_executor.py). Issue #84 wires it into create_app via build_openclaw_node_executor: the concrete executor is used when BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URL is set, and UnavailableOpenClawNodeExecutor otherwise (see Settings validation and health below).

Client mechanism

The executor is an HTTP client of OpenClaw Gateway's POST /tools/invoke endpoint - the Gateway's existing tool-invocation surface (same port as the Gateway, WS+HTTP multiplex, always enabled, gated by Gateway auth and tool policy). This is deliberately not the OpenAI-compatible /v1/chat/completions endpoint the OpenClaw LLM provider uses (OpenClawGatewayClient in bloom/integrations/llm/openclaw_provider.py): node execution is a tool call with a structured result, not a model completion, and reuses none of the chat/message plumbing.

Request fields

Bloom sends one bounded node-execution request per pilot node run:

{
"tool": "<bloom-to-openclaw tool name for this node>",
"args": { "...": "node input resolved from Bloom state, per node.reads" },
"sessionKey": "bloom:<thread_id>:node:<node_id>"
}
  • tool: the OpenClaw tool name for the node. The node-to-tool mapping is an explicit, reviewed allowlist owned by the executor implementation (#83) - never inferred from the workflow spec at runtime.
  • args: only the node's declared reads (see Workflow Spec To OpenClaw Execution Mapping), never full project/account state and never raw credentials.
  • sessionKey: Bloom's existing stable session key (bloom:<thread_id>:node:<node_id>, already computed in openclaw_runtime.py), reused for OpenClaw session continuity.
  • dryRun is not used by Bloom (the Gateway currently ignores it).

Response fields

  • 200 {"ok": true, "result": {...}} - result becomes the dict of Bloom-compatible writes returned from OpenClawNodeExecutor.run_tool; Bloom validates and applies it exactly as it would the built-in tool handler's return value.
  • 200 {"ok": false, "error": {"type": "...", "message": "..."}}, or a non-2xx HTTP status - both raise OpenClawNodeExecutionError. Bloom logs only the safe error type/class, never the raw message body, since it could echo request args.

Idempotency behavior

OpenClaw's /tools/invoke endpoint has no built-in request-level idempotency key (unlike the Gateway's side-effecting RPC methods such as send/agent). Idempotency is enforced entirely on Bloom's side, as already implemented in _OpenClawPilotContext._run_openclaw_tool:

  1. Bloom reserves a RuntimeCorrelation row keyed by a hash of the node's resolved input (_idempotency_key) before calling the executor.
  2. A cached succeeded correlation for the same key short-circuits the call entirely - the executor is never invoked twice for the same input.
  3. The executor itself must be safe to call at most once per reservation; it does not need its own idempotency key parameter because Bloom never re-issues a call for a succeeded correlation.

Timeout behavior

The executor's HTTP client uses BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_TIMEOUT_SECONDS (default 30s) as the request timeout. A timeout is treated the same as any other executor failure: it raises an exception that _OpenClawPilotContext._run_openclaw_tool catches, records the correlation as failed, and then either falls back to the built-in handler (fallback mode) or raises OpenClawNodeExecutionError (fail_closed mode). A timeout is never silently retried by the executor itself - Bloom's own job-retry policy (BLOOM_JOB_MAX_ATTEMPTS) is the only retry boundary above it.

Auth posture

BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_API_KEY, when set, is sent as Authorization: Bearer <key> on every request, matching OpenClaw's Gateway shared-secret auth. Per OpenClaw's own /tools/invoke documentation, a valid Gateway bearer credential is a full operator-level credential for that gateway instance, not a narrow per-tool scope - so:

  • the executor config is separate from the LLM-provider's BLOOM_OPENCLAW_API_KEY/ BLOOM_OPENCLAW_BASE_URL (see "Config separation" below); a leaked or overscoped LLM-provider key must not also grant orchestration-executor access, and vice versa.
  • the key is never logged, never returned in /health, and never included in issue/PR text.
  • the preferred deployment boundary stays private (loopback or Tailscale/tailnet), matching the existing "Authorization" section above.

Unsupported-node behavior

The executor is only ever called for nodes in BLOOM_OPENCLAW_ORCHESTRATION_PILOT_NODES; the runtime seam (_OpenClawPilotContext.run_tool) routes every other node id through the built-in WorkflowContext unconditionally, and human/event_wait/subworkflow node types and GitHub/Telegram-mutating tools stay built-in per Unsupported Or Built-In-Only Behavior. The executor implementation must reject (raise, never silently no-op) a request for a node it does not recognize; Bloom's fallback/fail-closed handling is the only path that decides what happens next.

Config separation

ConcernLLM-provider config (BLOOM_LLM_PROVIDER=openclaw, M8)Orchestration-runtime executor config (M10-1)
Selectswhich backend structured() calls route throughwhich backend allowlisted workflow nodes execute through
Base URLBLOOM_OPENCLAW_BASE_URLBLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URL
API keyBLOOM_OPENCLAW_API_KEYBLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_API_KEY
Timeoutfixed in OpenClawGatewayClient (60s)BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_TIMEOUT_SECONDS (default 30s)
Required whenBLOOM_LLM_PROVIDER=openclawBLOOM_ORCHESTRATION_RUNTIME=openclaw_pilot and BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fail_closed
Consumed bybloom.integrations.llm.factory / OpenClawLLMProviderthe concrete OpenClawNodeExecutor (issue #83)

These two configs may point at the same physical Gateway, but Bloom never reads one to satisfy the other - each is validated and consumed independently, so a change to one can never silently change the other's behavior.

M10-4 Execution Harness

Issue #85 adds an integration harness that runs the track_progress workflow path through OpenClawPilotRuntime with the concrete executor (#83/#84) rather than a fake executor that bypasses it. The reusable rig lives in bloom/workflow/openclaw_track_progress_harness.py; the tests are in tests/integration/test_openclaw_track_progress_harness.py.

The harness wires OpenClawGatewayNodeExecutor/OpenClawExecutorGatewayClient to a caller-supplied httpx.AsyncClient, so the exact same code path can be driven two ways:

  • CI (default): the client's transport is httpx.MockTransport, so the harness proves request shape, response validation, idempotency-key reuse, cached-success reuse, and Bloom's own reducer application with no real network and no credentials.
  • Optional live check: pointing the client at a real local OpenClaw gateway's base URL, gated behind BLOOM_OPENCLAW_EXECUTOR_LIVE_TEST=1 (see apps/api/README.md). Never runs in CI; both the enabling env var and the gateway base URL must be set explicitly.

What the harness proves:

  • Request shape: the POST /tools/invoke body's tool, sessionKey, and args (threadId, idempotencyKey, bounded input) match the Request fields contract above.
  • Idempotency key reuse and cached-success reuse: a second run with identical resolved input reuses the RuntimeCorrelation reserved by the first run and never calls the gateway again (see Idempotency above).
  • Validated writes: only the executor's validated result (per _validate_node_response) reaches Bloom state.
  • Bloom-owned reducer application: the write lands in run state through the field's declared reducer (progress uses merge, matching the real workflow spec), via Engine.advance's apply_writes - not a passthrough assignment.
  • Correlation status lifecycle: openclaw_runtime.node_started (running) is followed by either openclaw_runtime.node_succeeded (succeeded) or openclaw_runtime.node_failed (failed); a duplicate run for the same input instead logs openclaw_runtime.cached_result without transitioning the correlation again.
  • Fallback regression: a concrete-executor tool failure or a malformed gateway response both still reach the built-in track_progress handler in fallback mode, and both still raise OpenClawNodeExecutionError in fail_closed mode - unchanged from the M9 pilot's fallback tests in tests/unit/test_runtime.py.

Settings validation and health

  • Settings fails fast (at construction/startup, not at first node execution) when BLOOM_ORCHESTRATION_RUNTIME=openclaw_pilot and BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fail_closed are set without BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URL. The error names the missing variable so an operator can fix it without reading source.
  • fallback mode never requires this config - it remains deployable with no OpenClaw executor present at all, matching the existing pilot rollout guidance above.
  • /health's orchestration.concrete_executor object reports configured (whether BLOOM_OPENCLAW_ORCHESTRATION_EXECUTOR_BASE_URL is set) and available (whether the runtime's wired executor actually reports itself available). Since #84, create_app builds OpenClawGatewayNodeExecutor whenever configured is true, so available tracks configured in every openclaw_pilot deployment; configured=true, available=false cannot happen (fail_closed without config fails Settings construction instead, and fallback without config falls straight to UnavailableOpenClawNodeExecutor, which reports configured=false, available=false). Neither the configured nor available field, nor any other /health output, ever exposes the executor base URL or API key.

M10-5 State-Only Agent Node Pilot

Issue #86 extends the OpenClaw pilot from tool nodes only to one state-only agent node, so OpenClaw is not merely a read-only tool shim. Bloom's built-in engine still owns graph traversal, business gates, and reducer application; the pilot node's OpenClaw execution still returns nothing but a validated dict of Bloom writes.

Selected node and why it is safe

prd_authoring is the pilot agent node. See Current Bloom SDLC Node Classification for the full rationale; in short:

  • It writes exactly one state field (prd), never conversation or inbound_message.
  • It never sends anything to the user - the downstream request_prd_approval human node (always built-in) is what turns prd.summary_for_user into an outbound Telegram message, gated by Bloom's own approval semantics.
  • It has no external side effects (no GitHub/Telegram calls).

Broader capability policy/safety gates beyond this single-node pilot are out of scope for this issue (tracked separately, issue #87).

Routing: explicit by node id, never by node type

OpenClawPilotRuntime now takes a second allowlist, pilot_agent_nodes, alongside the existing pilot_nodes (tool allowlist). _OpenClawPilotContext.run_agent checks node.id against pilot_agent_nodes before ever calling the executor - exactly mirroring run_tool's existing pilot_nodes check, never inferring routeability from node.type == "agent" alone. An agent node not named in the allowlist keeps using the built-in WorkflowContext.run_agent binding unconditionally, the same as any other unsupported node.

Both run_tool and run_agent now share one _run_openclaw_node helper in bloom/workflow/openclaw_runtime.py for correlation reservation, idempotency-key computation/reuse, structured logging, and fallback/fail-closed dispatch - the only thing that differs between the two call sites is which executor method and which built-in delegate method is invoked.

Config surface:

  • BLOOM_OPENCLAW_ORCHESTRATION_PILOT_AGENT_NODES (default empty - unlike the tool allowlist, no agent node is piloted out of the box; an operator must opt one in explicitly).
  • The same BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE (fallback/fail_closed) governs agent-node failures identically to tool-node failures.

Executor: same /tools/invoke transport, a second allowlist

OpenClawGatewayNodeExecutor gains a run_agent method that posts to the same POST /tools/invoke endpoint as run_tool - node execution is "a tool call with a structured result" regardless of the Bloom node's type (see the M10-1 contract above). It is checked against a separate, explicit allowlist, DEFAULT_NODE_AGENT_MAP ({"prd_authoring": "prd_authoring"} by default), so a node in the tool allowlist can never be reached via run_agent or vice versa. An unrecognized node id is rejected with OpenClawNodeExecutionError before any request is made, exactly like the existing tool-node rejection.

The request-building and response-validation code is shared with run_tool (_invoke, _build_args, _validate_node_response):

  • Bounded input: only node.reads resolved from state (resolve_node_reads), never full project/account state - identical to the tool path.
  • Expected structured output: args.outputSchema is now resolved from either a top-level node.config.outputSchema or, since the real workflow spec puts an agent node's schema under nested config.output.schema (see prd_authoring's node config in workflows/bloom-sdlc.workflow.json), that nested shape - so the request carries the same schema Bloom's built-in agent binding expects the LLM to satisfy.
  • Secret filtering: the same key-name-based secret filter (_without_secret_like_dict_keys) applies to agent-node args as it does to tool-node args.
  • Output validation: a non-dict result, or any result key whose top-level field is not in node.writes, is rejected with OpenClawNodeExecutionError before Bloom ever applies a reducer - identical to the tool path's write-key allowlist enforcement.

Fallback/fail-closed behavior

Unchanged from the tool-node pilot: on an agent-node OpenClaw failure (network error, invalid response, unexpected write key, etc.), fallback mode calls the built-in WorkflowContext.run_agent binding (e.g. the real ProductOwnerAgent.author_prd path in Orchestrator) and logs the failure; fail_closed raises OpenClawNodeExecutionError instead. Side-effecting and unsupported node ids - human, event_wait, GitHub/Telegram-mutating tool nodes, and every other agent node not explicitly named in BLOOM_OPENCLAW_ORCHESTRATION_PILOT_AGENT_NODES - are never routed through OpenClaw by this change.

Tests

  • tests/unit/test_runtime.py: agent-node routing, correlation recording, allowlist-miss regression (agent node present but not allowlisted stays built-in), fallback, and fail-closed.
  • tests/unit/test_openclaw_executor.py: agent-node success and request shape (bounded input, nested output schema, secret-safe args), unknown-agent-node rejection, tool-allowlist-does-not- satisfy-agent-routing regression, non-dict result rejection, unexpected write key rejection.
  • tests/integration/test_openclaw_agent_pilot.py: an offline (httpx.MockTransport) integration test proving prd_authoring runs end to end through OpenClawPilotRuntime -> OpenClawGatewayNodeExecutor -> OpenClawExecutorGatewayClient, that Bloom applies the validated write through the prd field's merge reducer, and that invalid structured output/failure both still fall back (or fail closed) exactly like the tool-node pilot.

M10-6 Capability Policy And Safety Gates

Issue #87 adds a centralized, code-owned capability policy (bloom/workflow/openclaw_policy.py, OpenClawCapabilityPolicy) so that enabling OpenClaw orchestration - or misconfiguring the operator allowlist - cannot silently route an unsupported or side-effecting workflow node through OpenClaw. This is a third, independent gate, above OpenClawPilotRuntime's existing pilot_nodes/pilot_agent_nodes allowlist (config) and below OpenClawGatewayNodeExecutor's own DEFAULT_NODE_TOOL_MAP/DEFAULT_NODE_AGENT_MAP (executor). All three must independently allow a node before it ever reaches OpenClaw:

configured allowlist (Settings) -> capability policy (this section) -> executor allowlist

What the policy checks

For every node opted into pilot_nodes/pilot_agent_nodes, _OpenClawPilotContext calls OpenClawCapabilityPolicy.evaluate_tool/evaluate_agent before any executor call. The policy checks, in order:

  1. Node type - only tool and agent nodes are ever routable; human, event_wait, map, router, passthrough, terminal, and subworkflow are always rejected, matching Unsupported Or Built-In-Only Behavior.
  2. Side-effect posture - a tool node whose declared config.integration/config.action names a known side-effecting integration (telegram, deploy, ci) or GitHub-mutating action (issue/milestone create-or-close, PR review publication, merge) is rejected outright, regardless of node id.
  3. The policy's own hard-coded node set (POLICY_ALLOWED_TOOL_NODES, POLICY_ALLOWED_AGENT_NODES) - exactly track_progress and prd_authoring, the full M10 node set (see Exact M10 Routable Node Set below). Changing this set means editing openclaw_policy.py and its tests, never a runtime config value.
  4. The operator-configured allowlist - a node must be in both the hard-coded set and Settings.openclaw_orchestration_pilot_node_names/..._pilot_agent_node_names to route.
  5. Output validation - the node's declared writes must be a subset of the fields the policy allows that node id to write (track_progress -> progress, prd_authoring -> prd), and track_progress's declared config.integration/config.action must still match the read-only pair the policy was written against (github/list_project_status).

Fail-closed vs. fallback on policy rejection

A node not in the configured allowlist at all was never a routing candidate, so it silently uses the built-in binding - unchanged M9/M10-5 behavior, no new logging. A node that is configured but fails the policy is a genuine, actionable event: _OpenClawPilotContext logs openclaw_policy.rejected (node id, node type, decision class, and a static, secret-safe reason - never state or input) and then applies the same BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE semantics as an executor failure - fallback runs the built-in handler, fail_closed raises OpenClawNodeExecutionError before any executor call. Silent fallback is never allowed for a policy rejection.

Observability

OpenClawPilotRuntime.policy_summary (and /health's orchestration.policy field) expose the policy's hard-coded and configured node-id allowlists, the last decision (node id, node type, allowed, decision class), and a count per decision class - node ids and decision classes only, never node state, input, or secrets.

Exact M10 Routable Node Set

At the end of M10, exactly these two nodes may route through OpenClaw (and only when also present in the operator's BLOOM_OPENCLAW_ORCHESTRATION_PILOT_NODES/..._PILOT_AGENT_NODES):

  • track_progress (tool): read-only GitHub status refresh, writes only progress.
  • prd_authoring (agent): state-only PRD draft, writes only prd.

Every other node in workflows/bloom-sdlc.workflow.json remains built-in-only for M10, enforced by the capability policy regardless of any future config change:

  • human: ask_clarification, request_prd_approval.
  • event_wait: await_message, await_project_event.
  • agent (not policy-allowlisted): requirement_discovery, milestone_planning, ticket_generation, review_one_pull_request, gather_feedback.
  • passthrough: freeze_prd.
  • map: sync_tickets, review_pull_requests.
  • tool (side-effecting): sync_milestones (GitHub milestone create), create_issue_for_ticket (GitHub issue create), notify_planning_ready, notify_review (Telegram notify), deploy (deployment trigger), run_tests (CI trigger).

Implementing any newly allowed side-effecting action, or expanding the M10 routable set, is out of scope for this issue and requires a deliberate change to openclaw_policy.py plus new tests.

M10-7 Flip-Readiness Runbook

Issue #88 adds a machine-checkable readiness gate and runbook, run before an operator switches a real environment's BLOOM_ORCHESTRATION_RUNTIME to openclaw_pilot - especially before BLOOM_OPENCLAW_ORCHESTRATION_FAILURE_MODE=fail_closed, where the built-in fallback no longer absorbs a misconfiguration. This is the last of the M10 issues (#82-#88): the concrete executor (M10-1..M10-4), the second pilot node (M10-5), and the capability policy (M10-6) are all prerequisites this check verifies are actually wired together correctly for the environment about to be flipped, not just individually correct in isolation.

The command is bloom.workflow.openclaw_readiness (bloom/workflow/openclaw_readiness.py), run via uv run python -m bloom.workflow.openclaw_readiness. It builds the exact Settings/ create_app wiring the target process would boot with - in-process, via TestClient, no separate server - and checks, in order:

  1. Settings constructs without error (exercising the existing fail_closed-without-executor-url guard in bloom/config.py).
  2. The configured pilot_nodes/pilot_agent_nodes allowlists are each a subset of POLICY_ALLOWED_TOOL_NODES/POLICY_ALLOWED_AGENT_NODES (openclaw_policy.py) - the same check the capability policy itself performs at runtime, run here ahead of time.
  3. For fail_closed: the executor base URL is configured (redundant with #1's guard, but reported as its own named check for a clear pass/fail line).
  4. GET /health (in-process) reports status: ok.
  5. orchestration.active_runtime == "openclaw_pilot" and openclaw_enabled == true.
  6. orchestration.concrete_executor.configured and .available are both true.
  7. orchestration.correlation_store.healthy is true.
  8. orchestration.policy's configured_tool_nodes/configured_agent_nodes match the settings used to build the app - catching stale runtime wiring, not just stale config.
  9. The offline track_progress execution harness (openclaw_track_progress_harness.py, M10-4) runs end to end through the real OpenClawPilotRuntime -> OpenClawGatewayNodeExecutor -> OpenClawExecutorGatewayClient code path, via httpx.MockTransport - no live gateway or network access.
  10. The same harness against the real configured executor base URL.
    • Required by default for openclaw_pilot + fail_closed. /health's concrete_executor.available reflects wiring/config only, not real reachability, so it is not sufficient on its own - a fake or unreachable executor URL must (and now does) report NOT READY. --skip-live-gateway (alias --offline-only) is the explicit escape hatch for CI/docs dry runs: it is always visibly marked in the report (offline_only_simulation: true) and in the final banner, and it never approves a fail_closed production flip.
    • Everywhere else (fallback mode, or builtin runtime), it stays optional and explicitly marked as such in the output: opt in with BLOOM_OPENCLAW_EXECUTOR_LIVE_TEST=1 together with an executor base URL - mirrors the M10-4 harness test file's own opt-in live check.

--runtime/--failure-mode/--pilot-nodes/--pilot-agent-nodes/--executor-base-url flags override the corresponding Settings field for that invocation only (never written to .env or the environment), so the same command can preview a flip against a target environment's real config without changing it, or validate whatever is already configured when run with no flags.

The command exits 0 only when every required check passes and 1 otherwise, prints one [OK]/[FAIL]/[WARN] line per check, and supports --json for a machine-readable report. No check output ever includes an API key, gateway URL, prompt, or model response - see tests/integration/test_openclaw_readiness.py's test_no_secret_ever_appears_in_the_report for the regression test. See docs/deployment.md for the full operator-facing runbook (staged rollout, production enablement, and log events to watch).

Webhook Acknowledgment Timing

Telegram and GitHub webhook routes should continue to:

  1. Verify the inbound request.
  2. Map it to a project thread when needed.
  3. Enqueue a durable Bloom job.
  4. Return quickly.

OpenClaw execution starts only after the Bloom job runner claims the durable job. This preserves the current fast-ack behavior and avoids coupling third-party webhook delivery to OpenClaw availability.

Authorization

The OpenClaw gateway token is an operator-level credential. Bloom may hold it only in the backend process and must never expose it to the dashboard, browser, issue bodies, logs, or PR comments.

The preferred deployment boundary is private:

  • loopback when Bloom and OpenClaw run on the same host,
  • Tailscale or equivalent private network when they are separate,
  • no public unauthenticated OpenClaw endpoint.

Future per-user OpenClaw authorization can be added later, but M9 should treat OpenClaw as a trusted backend runtime called by Bloom.

Observability

Runtime execution should emit structured, secret-safe metadata:

  • runtime provider,
  • Bloom thread id,
  • Bloom job id,
  • workflow node id,
  • event key or stable hash,
  • OpenClaw session key or execution id,
  • status,
  • duration,
  • retry count,
  • safe error class and message.

Do not log prompts, model responses, tokens, API keys, gateway tokens, or raw third-party payloads that may contain sensitive user data.

Dashboard and health surfaces should make the active runtime visible and show whether OpenClaw correlation storage is healthy.

The M9 implementation exposes this through /health:

  • configured runtime,
  • active runtime,
  • whether OpenClaw orchestration is enabled,
  • pilot node allowlist,
  • failure mode,
  • correlation-store backend and health.

The pilot runtime logs node lifecycle events with runtime, thread id, node id, OpenClaw session key, idempotency key, status, fallback decision, and safe error class. It does not log OpenClaw gateway credentials, prompts, responses, or state payloads.

Rollback

The default runtime remains builtin.

Runtime selection should be controlled by configuration and should allow Bloom to switch back to the built-in runtime without a schema migration. If a rollback happens while OpenClaw work is in flight, Bloom should mark those correlations as abandoned or ignored, then resume from persisted Bloom state through the built-in runtime.

Rollback must never duplicate GitHub side effects blindly.

Implemented rollback control:

BLOOM_ORCHESTRATION_RUNTIME=builtin

Switching back to builtin routes every workflow node through Bloom's built-in runtime and leaves runtime-correlation rows as audit records. No schema migration is required.

Incremental Migration Plan

Each step is stoppable and should land as its own issue-sized PR.

  1. Document this boundary design.
  2. Map Bloom workflow node types to OpenClaw runtime capabilities and identify unsupported node behavior that must remain built-in. See Workflow Spec To OpenClaw Execution Mapping.
  3. Add the OrchestrationRuntime seam and wire BuiltInRuntime as the default, with no behavior change.
  4. Add OpenClaw correlation persistence and idempotency records.
  5. Add an OpenClawRuntime adapter behind a disabled-by-default feature flag. Start with tests and fake OpenClaw responses before any production path.
  6. Pilot one low-risk, non-mutating workflow path through OpenClaw, then add health checks, metrics, and rollback controls.
  7. Reconcile PRD, architecture docs, workflow docs, and operations runbooks.
  8. Define the concrete OpenClaw executor contract and its config surface (M10-1, this document's Concrete Executor Contract section). Building the real gateway-backed executor against that contract is a separate, later issue (#83).
  9. Build the gateway-backed OpenClawNodeExecutor implementation against the M10-1 contract (M10-2, issue #83): OpenClawGatewayNodeExecutor and OpenClawExecutorGatewayClient in bloom/workflow/openclaw_executor.py.
  10. Wire the concrete executor into create_app behind a config-driven factory, keeping UnavailableOpenClawNodeExecutor for the no-config fallback case, and expose its real availability through /health (M10-3, issue #84).
  11. Add an integration harness that proves track_progress end-to-end through the concrete executor - request shape, idempotency/cached-success reuse, validated writes, Bloom's reducer application, and unchanged fallback behavior - plus an opt-in live-gateway check (M10-4, issue #85, this document's Execution Harness section).
  12. Extend the pilot to one state-only agent node (M10-5, issue #86): prd_authoring, routed explicitly by node id through a second, opt-in allowlist, with the same bounded-input, validated-output, fallback/fail-closed semantics as the tool-node pilot (this document's State-Only Agent Node Pilot section).
  13. Add a centralized capability policy and safety gates (M10-6, issue #87): OpenClawCapabilityPolicy gates node type, side-effect posture, the policy's own hard-coded node set, the configured allowlist, and declared output/writes before any executor call - see this document's Capability Policy And Safety Gates section. Enabling any newly allowed side-effecting action is out of scope and left for a later issue.
  14. Add a flip-readiness command and operator runbook (M10-7, issue #88): a machine-checkable gate that verifies config, /health, executor availability, correlation-store health, and an OpenClaw execution harness before an operator switches an environment to openclaw_pilot - see this document's M10-7 Flip-Readiness Runbook section and docs/deployment.md. M10 is complete as of this issue; the recommended next expansion candidate is noted in Pilot Path Recommendation below and in docs/milestones/m10-openclaw-concrete-executor-and-safety-gates.md.

Pilot Path Recommendation

The first production-adjacent pilot should be read-only or advisory. Avoid initially routing GitHub-mutating nodes such as issue creation, label updates, PR review publication, milestone closure, or merge decisions through OpenClaw.

The implemented first pilots are:

  • track_progress: a read-only GitHub status refresh tool node that writes Bloom's progress field.
  • prd_authoring: a state-only agent node (M10-5) that writes only Bloom's prd field and never messages the user directly.

Good later candidates:

  • a status or monitoring answer path that only reads Bloom and GitHub state,
  • an advisory specialist-review draft before it is published,
  • a fake-runtime integration test path that exercises agent dispatch without external side effects.

The pilot should prove routing, correlation, retries, and observability before OpenClaw is allowed to execute side-effecting workflow nodes.

Non-Goals For M9

  • Moving webhook routing into OpenClaw.
  • Moving account/session storage into OpenClaw.
  • Making OpenClaw the product state store.
  • Removing the built-in runtime.
  • Routing all workflow nodes through OpenClaw at once.
  • Exposing the OpenClaw gateway credential to clients.