Skip to main content

Workflow Control Plane

"LangChain runs the workflow. AxonFlow decides when it's allowed to move forward."

The Workflow Control Plane provides governance gates for external orchestrators like LangGraph, LangChain, and CrewAI. Teams also use it with custom orchestrators, workflow engines, schedulers, and internal agent frameworks by registering them as source: external.

Overview

External orchestrators are great at workflow execution, but production AI systems still need policy enforcement, approvals, traceability, and audit evidence. The Workflow Control Plane solves this by providing:

  1. Step Gates - Policy checkpoints before each workflow step
  2. Decision Types - Allow, block, or require approval
  3. Policy Integration - Reuses AxonFlow's policy engine
  4. Audit Trail - Every step decision is recorded

How It Works

Key Point: Your orchestrator runs the workflow. AxonFlow provides governance gates at each step transition.

Quick Start

1. Start AxonFlow

docker compose up -d

2. Create a Workflow

curl -X POST http://localhost:8080/api/v1/workflows \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \
-d '{
"workflow_name": "code-review-pipeline",
"source": "langgraph",
"trace_id": "langsmith-run-abc123"
}'

Note: total_steps is optional. Omit it — the server automatically sets it to the actual step count when the workflow reaches a terminal state (completed, aborted, or failed). This supports dynamic workflows like LangGraph where the number of steps is not known upfront.

Response:

{
"workflow_id": "wf_abc123",
"workflow_name": "code-review-pipeline",
"status": "in_progress"
}

3. Check Step Gate

Before executing each step, check if it's allowed:

curl -X POST http://localhost:8080/api/v1/workflows/wf_abc123/steps/step-1/gate \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \
-d '{
"step_name": "Generate Code",
"step_type": "llm_call",
"model": "gpt-4",
"provider": "openai"
}'

Response (allowed):

{
"decision": "allow",
"step_id": "step-1",
"policies_evaluated": [
{
"policy_id": "sys_dyn_llm_cost",
"policy_name": "LLM Cost Optimization",
"action": "alert"
}
],
"cached": false,
"decision_source": "fresh",
"retry_context": {
"gate_count": 1,
"completion_count": 0,
"prior_completion_status": "none",
"prior_output_available": false,
"prior_output": null,
"prior_completion_at": null,
"first_attempt_at": "2026-04-21T15:30:45.123Z",
"last_attempt_at": "2026-04-21T15:30:45.123Z",
"last_decision": "allow",
"idempotency_key": ""
}
}

retry_context is present on every gate response and carries first-class retry state: attempt counters, prior completion status, and the caller-supplied idempotency key if one was set. See Retry Semantics & Idempotency for the full field reference, the include_prior_output query parameter, and migration guidance from the legacy cached / decision_source fields.

Response (blocked):

{
"decision": "block",
"step_id": "step-1",
"reason": "GPT-4 not allowed in production",
"policy_ids": ["policy_gpt4_block"]
}

4. Complete Workflow

curl -X POST http://localhost:8080/api/v1/workflows/wf_abc123/complete \
-H "Authorization: Basic $(echo -n 'client-id:client-secret' | base64)" \

Gate Decisions

DecisionDescriptionAction
allowStep is allowed to proceedExecute the step
blockStep is blocked by policySkip or abort workflow
require_approvalHuman approval requiredWait for approval (evaluation tier and enterprise)

Step Types

TypeDescriptionExample
llm_callLLM API callOpenAI, Anthropic, Bedrock
tool_callTool/function executionCode execution, file operations
connector_callMCP connector callDatabase, API integrations
human_taskHuman-in-the-loop taskManual review, approval

Workflow Sources

SourceDescription
langgraphLangGraph workflow
langchainLangChain workflow
crewaiCrewAI workflow
externalCustom orchestrator, workflow engine, scheduler, or internal framework

API Reference

MethodEndpointDescription
POST/api/v1/workflowsCreate workflow
GET/api/v1/workflows/{id}Get workflow status
POST/api/v1/workflows/{id}/steps/{step_id}/gateCheck step gate
POST/api/v1/workflows/{id}/steps/{step_id}/completeMark step completed (optional output + usage metrics)
POST/api/v1/workflows/{id}/completeComplete workflow
POST/api/v1/workflows/{id}/abortAbort workflow
POST/api/v1/workflows/{id}/failFail workflow
POST/api/v1/workflows/{id}/resumeResume workflow
GET/api/v1/workflowsList workflows

Evaluation tier and enterprise approval workflows add:

  • POST /api/v1/workflows/{id}/steps/{step_id}/approve
  • POST /api/v1/workflows/{id}/steps/{step_id}/reject
  • GET /api/v1/workflows/approvals/pending — WCP-plane pending-approvals listing

MAP (multi-agent plan) mode exposes a plane-scoped mirror of these endpoints at /api/v1/plans/... — same response shapes plus a plan_id field on every entry. Reviewer tools that integrate against either plane do not need to branch on path. See HITL Approval Gates for the full API reference, or ADR-046 for the parity rule.

Step Completion Payload

POST /api/v1/workflows/{id}/steps/{step_id}/complete accepts an optional JSON body. You can send:

  • output -- structured step output object
  • tokens_in -- actual input tokens consumed
  • tokens_out -- actual output tokens produced
  • cost_usd -- actual cost for the step
  • idempotency_key -- optional string that must match the key (if any) that was passed to the gate call for this step. A mismatch returns 409 IDEMPOTENCY_KEY_MISMATCH. See Retry Semantics & Idempotency for the mismatch rules and the error shape.

If omitted, the endpoint still succeeds and marks the step complete. The response is 204 No Content.

Fail Workflow

failWorkflow() terminates a workflow as failed with an optional reason. Unlike abortWorkflow(), which indicates a manual cancellation (e.g., due to a policy block), failWorkflow() indicates an error condition -- the workflow encountered an unrecoverable problem during execution.

After a workflow is failed, its status becomes failed and it cannot be resumed.

POST /api/v1/workflows/{id}/fail

Request Body:

{
"reason": "optional failure reason"
}

Response:

{
"workflow_id": "wf_abc123",
"status": "failed",
"reason": "optional failure reason"
}

SDK Examples:

// Go
err := client.FailWorkflow(workflowID, "pipeline error: step 3 timed out")
# Python
await client.fail_workflow(workflow_id, reason="pipeline error: step 3 timed out")
// TypeScript
await client.failWorkflow(workflowId, "pipeline error: step 3 timed out");
// Java
client.failWorkflow(workflowId, "pipeline error: step 3 timed out");

Community vs Enterprise

FeatureCommunityEnterprise
Step gates (allow/block)YesYes
Policy evaluationYesYes
SDK support (4 languages)YesYes
LangGraph adapterYesYes
Generic external sourceYesYes
require_approval actionReturns decisionAdds approval endpoints and portal workflows
Org-level policiesNoYes
Cross-workflow analyticsNoYes

Troubleshooting

Gate Returns "allow" When Expected to Block

  1. Check if the policy exists and is enabled
  2. Verify the policy scope is workflow
  3. Check if conditions match the step request

Workflow Stuck in "in_progress"

  1. Ensure you call complete_workflow() or abort_workflow()
  2. Check for unhandled exceptions in your code
  3. Use the context manager or adapter helpers for automatic cleanup

Connection Refused

  1. Ensure AxonFlow Agent is running: docker compose ps
  2. Check the endpoint URL matches your configuration
  3. Verify network connectivity

Examples

See the complete examples in examples/workflow-control/:

  • http/workflow-control.sh - HTTP/curl example
  • go/main.go - Go SDK example
  • python/main.py - Python SDK example
  • python/langgraph_example.py - LangGraph adapter example
  • python/langgraph_wrapper_example.py - Wrapper-based LangGraph integration
  • python/langgraph_tools_example.py - Per-tool governance example
  • typescript/index.ts - TypeScript SDK example
  • typescript/langgraph_tools_example.ts - TypeScript per-tool governance example
  • java/WorkflowControl.java - Java SDK example
  • java/LangGraphToolsExample.java - Java per-tool governance example