Workflow startup and recovery¶
Status: Current | Last reviewed: 2026-10-13
The first workflow expansion package gives direct HTTP and agent-host MCP requests one durable admission path, reserves interactive worker capacity, and measures startup through the first useful node. It retains LangGraph and the existing RabbitMQ topology. No gateway, routing model or team framework is added.
Admission and execution¶
POST /api/v1/workflows/{id}/execute with execution_mode: "async" commits
workflow_executions and a private scheduled_jobs dispatch envelope in one
transaction. It immediately publishes a persistent RabbitMQ job with mandatory
routing and publisher confirmation. Only confirmed publication marks the
dispatch row dispatched. A failed or uncertain publish leaves it pending;
the HTTP process's recovery loop retries eligible rows every second.
The response remains HTTP 200, status: "submitted", with an execution_id.
This receipt means durable acceptance. It does not assert that a worker has
started. Healthy publication is immediate; a broker outage can consume the
five-second publish timeout before the receipt returns.
Optional Idempotency-Key deduplicates direct asynchronous submissions within
a tenant/workflow. Identical inputs, caller chain and execution overrides join
the same execution; changed arguments return 409. Evaluation attempts are
isolated from one another. Keys last as long as their execution records.
The admitted root configuration/version is copied into the private dispatch envelope. Resume jobs recover it from that envelope. Nested workflows still resolve through the existing provider; this is not a transitive dependency snapshot. Execution-time authorization and credentials remain separate from the configuration snapshot.
Keep dispatched envelopes for the lifetime of resumable executions. They can
contain evaluation capabilities and must not be exposed as public execution
metadata. The generic scheduled-job poller excludes these canonical envelopes.
Event intake, schedules and voice /prepare retain their existing admission
paths; they are not converted to transactional outbox producers by this package.
New asynchronous runs are pending, with null started_at, while setup runs.
The first non-start/end/passthrough node wrapper records the start time and
publishes the existing started progress event. The Test panel renders an
absent start time as Pending.
Capacity and readiness¶
| Setting | Default | Meaning |
|---|---|---|
WORKFLOW_JOB_CONCURRENCY |
4 |
Background workflow slots and queue prefetch |
WORKFLOW_INTERACTIVE_CONCURRENCY |
2 |
Additional priority >=5 workflow slots and queue prefetch |
WORKFLOW_WORKER_LANE |
all |
all, interactive, or background queue consumption |
RabbitMQ uses independent channels and semaphores for the two job lanes.
Normal work cannot occupy reserved interactive slots. Direct HTTP/MCP jobs
use priority 5; existing priority producers share that capacity.
An all process now permits six full workflow jobs instead of four.
This consumes additional resources; the measured gain is not a same-capacity
comparison. Per-provider/model limits continue to apply.
For stronger process isolation, run separate instances with
WORKFLOW_WORKER_LANE=interactive and background, sharing dependencies.
Interactive instances omit scheduler, batch, Salesforce Bulk and fanout
pollers. Both roles still host HTTP and dispatch recovery; this package adds
the role setting, not a second chart deployment. Keep background capacity
running when choosing interactive-only instances.
HTTP startup and consumer startup warm PostgreSQL, Redis and the required Postgres checkpointer. Missing checkpoint support or initialization failure prevents successful startup; execution no longer silently loses durability. Database and Redis first initialization are serialized within the process. Tool loading and graph compilation still occur per run.
Recovery guarantees and limits¶
Execution leases use random ownership tokens, periodic renewal and compare-and-delete release. Lease loss cancels the local task and requeues the job. A stale owner cannot release a successor's lease.
Duplicate start jobs reuse a stable execution/run. Completed or suspended starts are not restarted. Interrupted active work resumes the saved checkpoint, including cancellation before graph entry. Jobs without an execution ID derive a stable identity from their job ID. Resume job IDs are recorded in private checkpoint state so redelivery of one approval cannot approve a later gate. Broker shutdown cancels tracked workflow tasks and requeues interrupted work.
Cancellation is cooperative. An external write may succeed before its checkpoint commits; a node interrupted mid-call can run again. This is not exactly-once execution or external-system fencing. Business writes still need downstream idempotency and reconciliation. Ordinary application failures retain the existing error/DLQ behavior.
MCP callers¶
Generic and named agent-host workflow tools call the authenticated HTTP API.
They no longer create their own run or write the unused Redis job stream.
trigger_workflow returns execution_id; check_workflow_status accepts that
receipt or a run ID. Named tools add workflow_idempotency_key; generic tools
accept idempotency_key. Both map to the HTTP header.
wait_for_result polls the same durable execution for up to 120 seconds.
Timeout or polling failure preserves the receipt. Status and approval forward
the validated caller bearer and tenant. Only verified internal/evaluation
contexts can use the configured service credential fallback; the downstream
API still verifies tenant and scope. Refresh client tool schemas after rollout.
Set ASYNC_AGENT_URL on agent-host; its default is http://async-agent:8000.
The local launcher defaults to http://localhost:8000.
Observe startup¶
GET /api/v1/workflow-runs/{id} accepts either the submitted execution receipt
or a specific run UUID and enforces tenant authorization. It returns the
nullable run ID/timestamps, status, run metadata and execution SSE path.
Scrape /metrics on async-agent over the internal service network. It exposes
trueserve_workflow_startup_seconds{stage,lane} without tenant/execution labels.
Startup stages include authentication, durable admission, publication,
consumer entry, lease acquisition, run claim, configuration/tools/graph ready,
runtime entry and first useful node. Delivery and slot-acquisition timestamps
also travel in the worker context. workflow_runs.run_metadata.startup retains
the worker's known timestamps, first-node ID and lane.
The ASGI boundary records initial HTTP receipt and receipt of the complete body. Histograms measure elapsed time from complete-body receipt. First-node instrumentation timestamps entry into the node wrapper, before its startup bookkeeping and trace writes; it is not model first token or business output. The benchmark below measures entry into the actual assignment handler after that bookkeeping. Histograms use wall clocks across services, so synchronize hosts and account for clock skew. Scraping is process-local.
API-token authorization, expiry and membership checks still run per request.
Only the nonauthoritative last_used write is coalesced in the background,
at most once per minute per token per process.
Example p95 query:
histogram_quantile(
0.95,
sum by (le) (
rate(trueserve_workflow_startup_seconds_bucket{
stage="first_node",lane="interactive"
}[5m])
)
)
Inspect pending canonical dispatches with a read-only query:
SELECT count(*) AS pending, min(execute_at) AS oldest
FROM scheduled_jobs
WHERE status = 'pending' AND job_type = 'execution_start'
AND payload ? '_dispatch_job';
Local performance evidence¶
The before source was preserved before editing the application. Both versions use the same loopback HTTP harness, real API-token authentication, PostgreSQL, Redis, RabbitMQ and Postgres checkpoints. Synthetic workflows invoke native Assignment; background workflows wait 350 ms without models or business APIs. Ten warmups precede 200 idle requests and 100 mixed-load trials. Each mixed trial fills four background slots before submitting one interactive request. Trials wait for completion; this is not a constant-arrival capacity test.
Measured on 2026-10-13:
| Measure | Before p50 / p95 / p99 | After p50 / p95 / p99 |
|---|---|---|
| Idle request to native handler | 13.44 / 15.84 / 16.73 ms | 14.86 / 17.76 / 19.08 ms |
| Idle HTTP acceptance | 9.17 / 10.86 / 13.40 ms | 13.65 / 15.94 / 17.66 ms |
| Interactive start with four background jobs | 351.31 / 372.86 / 656.04 ms | 32.17 / 37.19 / 69.22 ms |
The mixed-load p95 falls about 90%. Four of 100 final mixed trials still exceed 50 ms; a shorter repeat recorded mixed p95 70.25 ms. The larger run supports the warm local p95 goal, but tail latency remains variable. Idle startup is slightly slower, and HTTP acceptance costs about 5 ms more at p95 because admission now persists recoverable dispatch intent.
Before results and after results include all raw samples, capacity settings and runtime source hashes. The sequential workload, shared devcontainer, no TLS/proxy and no model calls limit interpretation. These are not official ingress, burst-load, cold-start or post-guardrail measurements.
Reproduce in disposable dependencies¶
Run from the repository root. These fixed test endpoints and credentials are only for isolated fixtures. The schema includes Supabase roles/extensions, so the fixture uses the matching Supabase Postgres image.
docker run -d --name tsf-workflow-package-postgres \
-p 127.0.0.1:55439:5432 \
-e POSTGRES_PASSWORD=workflow-test-only -e POSTGRES_DB=workflow_package \
supabase/postgres:15.8.1.060
docker run -d --name tsf-workflow-package-redis \
-p 127.0.0.1:56389:6379 redis:7-alpine
docker run -d --name tsf-workflow-package-rabbitmq \
-p 127.0.0.1:56739:5672 \
-e RABBITMQ_DEFAULT_USER=workflow_test \
-e RABBITMQ_DEFAULT_PASS=workflow-test-only rabbitmq:3-management-alpine
Wait for PostgreSQL and RabbitMQ readiness, then load the disposable schema:
docker exec -e PGPASSWORD=workflow-test-only -i tsf-workflow-package-postgres \
psql -U supabase_admin -d workflow_package -v ON_ERROR_STOP=1 \
< deploy/init-scripts/000_full_schema.sql
docker exec -e PGPASSWORD=workflow-test-only tsf-workflow-package-postgres \
psql -U supabase_admin -d workflow_package -v ON_ERROR_STOP=1 \
-c "CREATE ROLE workflow_runner LOGIN SUPERUSER PASSWORD 'workflow-test-only'"
apps/async-agent/.venv/bin/python \
apps/async-agent/scripts/benchmark_workflow_startup.py \
--label after --samples 200 --mixed-samples 100 \
--output /tmp/workflow-startup-after.json
For a before/after comparison, preserve the earlier async-agent app directory
before edits and pass --source /absolute/path/to/apps/async-agent.
Its normal repository-relative libs and shared imports must remain available.
The benchmark binds only loopback port 18009 and records client-send-to-handler
and HTTP-response latency. Do not run two copies simultaneously.
Regression checks¶
With the fixtures running:
cd apps/async-agent
WORKFLOW_INTEGRATION_TESTS=1 .venv/bin/python -m pytest tests -q
cd ../agent-host
.venv/bin/python -m pytest tests -q
Executed results: async-agent 256 passed, 1 skipped; agent-host 70 passed. The eight opt-in integration cases cover publication recovery, idempotency conflicts, completed duplicate starts, checkpoint recovery after mid-node and pre-graph cancellation, renewed leases, stale ownership, duplicate human approval and authenticated tenant boundaries. MCP tests traverse the real SDK dispatch path with an HTTP transport stub; the separate benchmark exercises real HTTP and broker dependencies.
Critical Python lint and focused workflow-panel ESLint pass. A broader Python check also exposed a pre-existing unreachable JSON argument parser in the OpenAI batch adapter; it was repaired and covered with valid/invalid argument regressions. No model calls were needed. No official environment or external assistant client was exercised.
Remove only these fixtures when finished:
docker rm -fv tsf-workflow-package-postgres \
tsf-workflow-package-redis tsf-workflow-package-rabbitmq
Before production adoption, measure constant-arrival mixed load at the real ingress with declared resources, rates, payloads and tenants. Include TLS, cold starts, failure rate, p99, queue age and required routing/safety calls. Drain older worker versions before relying on duplicate-resume protection; older binaries do not honor the new lease and checkpoint contracts.
See also¶
Native safety-profile regression check¶
The October 13 native-guard package retains the no-policy startup path. A repeat of this benchmark with guards unconfigured measured idle p95 16.357 ms and mixed-load p95 37.007 ms. The preceding run overlapped development activity and recorded substantial mixed-load outliers; both runs and their limits are in the native safety report. Enabled model checks add their own latency and are not included in these results.