Skip to main content

Healthcare Prior Authorization — HITL + Idempotency

Prior authorization (PA) is the perfect case for human-in-the-loop AI. An agent can gather the documentation, build the clinical justification, and route the request to a payer — but the decision to commit a patient to a $4,200 procedure is exactly where an automated workflow has to pause and wait for a human with the license to sign off. This tutorial walks through that agent on AxonFlow's Workflow Control Plane, with two guarantees that matter for clinical use:

  • The approval cannot be retargeted. If the medical director approves procedure J0135 for patient P-88120, the agent physically cannot resume that approved step against a different patient or a different procedure — the idempotency_key match check rejects the call with 409 IDEMPOTENCY_KEY_MISMATCH before the PA submission fires.
  • The agent can tell, on any retry, whether the approval already landed or whether the PA submission already succeeded. retry_context surfaces attempt counts, the prior decision, and whether /complete ever fired — so a resumed agent doesn't re-submit an already-approved PA and doesn't re-trigger an already-granted approval.
Scope

This tutorial combines three capabilities:

  • retry_context and idempotency_key on step gates — new in platform v7.3.0, wire-level, available on every tier (including Community)
  • HITL approval queue for require_approval — available on Evaluation tier and above. Community can trigger require_approval decisions but cannot act on them (no queue API)
  • Retry-aware dynamic policies (optional extension at the end) — Evaluation tier and above

Run the core walkthrough on an Evaluation license. You can get a free one at getaxonflow.com/evaluation-license.

The scenario

Your PA agent is processing a request for patient P-88120, CPT code J0135 (adalimumab, a biologic that typically needs PA). The agent:

  1. Gathers the patient's clinical record and formulary notes
  2. Builds the PA request
  3. Pauses at a gate that triggers require_approval — the policy says "any biologic over $2,000 goes to medical director"
  4. Medical director reviews and approves
  5. Agent submits the PA to the payer
  6. /complete marks the step done

Along the way, the agent may retry — a WebSocket drop during reviewer polling, an orchestrator redelivery after a timeout, a resumed workflow after an agent restart. Every retry has to leave the approval pinned to patient P-88120 / procedure J0135. That's what idempotency_key buys you.

Flow

The critical moments are bolded in the diagram: the approval is bound to the pa:P-88120:J0135 key, and AxonFlow refuses any later gate or complete with a different key before the PA submission fires.

Prerequisites

  • AxonFlow running locally (Getting Started)
  • Evaluation license set via AXONFLOW_LICENSE_KEYfree registration. Needed for the HITL queue API to work; Community can return require_approval decisions but cannot enqueue them for reviewer action.
  • One AxonFlow SDK (Python, TypeScript, Go, Java) or curl
  • A policy named something like biologics-require-md-approval that returns require_approval on high-cost biologics (Step 0 below creates one)

Step 0 — Create the policy

On an Evaluation license, policies returning require_approval create HITL queue entries that the reviewer API can act on.

curl -X POST http://localhost:8080/api/v1/policies \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'pa-agent:your-secret' | base64)" \
-d '{
"name": "biologics-require-md-approval",
"description": "Biologic medication over $2,000 requires medical director approval",
"type": "context_aware",
"category": "dynamic-compliance",
"priority": 900,
"enabled": true,
"conditions": [
{"field": "step_input.medication_class", "operator": "equals", "value": "biologic"},
{"field": "step_input.estimated_cost_usd", "operator": "greater_than", "value": 2000}
],
"actions": [
{"type": "require_approval", "config": {"reason": "biologic medication over $2,000 requires medical director approval", "severity": "high"}}
]
}'

Note the policy shape: conditions is a flat array ANDed implicitly, actions is a plural array with each action wrapped in {type, config}, and the reason and severity live inside config. Operators are equals, greater_than, less_than, contains, not_equals, regex, in. The type must be context_aware (one of a fixed set) and category must start with dynamic- or media-. See WCP Policy Configuration for the full field reference.

This is a standard HITL policy — no retry-aware conditions yet. We'll stack one on top in the Evaluation-tier extension at the end.

Step 1 — Create the workflow and first gate

The idempotency key pa:P-88120:J0135 encodes the exact business identity of this PA: patient P-88120, procedure J0135. The key format is up to you — a hash of the full clinical request is also reasonable — but it must uniquely identify the transaction.

from axonflow import AxonFlow
from axonflow.workflow import (
CreateWorkflowRequest,
StepGateRequest,
StepType,
WorkflowSource,
)

IDEMPOTENCY_KEY = "pa:P-88120:J0135"

async with AxonFlow(
endpoint="http://localhost:8080",
client_id="pa-agent",
client_secret="your-secret",
) as client:
workflow = await client.create_workflow(
CreateWorkflowRequest(
workflow_name="prior-auth-request",
source=WorkflowSource.EXTERNAL,
trace_id="case-P-88120-J0135",
)
)

gate = await client.step_gate(
workflow_id=workflow.workflow_id,
step_id="submit-pa",
request=StepGateRequest(
step_name="Submit prior authorization",
step_type=StepType.TOOL_CALL,
step_input={
"patient_id": "P-88120",
"cpt_code": "J0135",
"medication_class": "biologic",
"estimated_cost_usd": 4200,
},
idempotency_key=IDEMPOTENCY_KEY,
),
)

# gate.decision == "require_approval"
# gate.approval_id is set (e.g. "app_318a270f...")
# gate.retry_context.gate_count == 1
# gate.retry_context.last_decision == "require_approval"

The agent is now paused. It does not submit anything to the payer.

Step 2 — Poll /gate while approval is pending

Your agent (or orchestrator, or scheduler) retries /gate periodically to check whether the reviewer has acted. Every retry returns the same require_approval decision with an incrementing gate_count, so the agent always knows how long it has been waiting and can decide to time out or escalate.

gate = await client.step_gate(
workflow_id=workflow.workflow_id,
step_id="submit-pa",
request=StepGateRequest(
step_name="Submit prior authorization",
step_type=StepType.TOOL_CALL,
idempotency_key=IDEMPOTENCY_KEY,
),
)

if gate.decision == "require_approval":
rc = gate.retry_context
print(f"Still pending. Attempt {rc.gate_count}, first_attempt_at={rc.first_attempt_at}.")
# prior_completion_status stays "gated_not_completed"
# last_decision stays "require_approval"
# idempotency_key stays "pa:P-88120:J0135"

The retry loop is intentionally boring. retry_context gives the agent everything it needs to decide "keep waiting" vs "escalate" without guessing — no side-channel state, no external coordination.

Step 3 — Medical director approves

The reviewer acts through the approval API (or the Enterprise Customer Portal UI in production deployments — see Customer Portal).

curl -X POST "http://localhost:8080/api/v1/workflows/$WORKFLOW_ID/steps/submit-pa/approve" \
-H "Authorization: Basic $(echo -n 'pa-agent:your-secret' | base64)" \
-H "X-User-ID: [email protected]"

Response:

{
"workflow_id": "wf_abc123",
"step_id": "submit-pa",
"approval_status": "approved",
"approved_by": "[email protected]",
"message": "Step approved"
}

The reviewer is identified from the X-User-ID header and recorded in the audit log. On rejection, the workflow aborts and the agent sees a rejected state on the next gate call. On expiry (24h default on Evaluation), the audit log records the auto-rejection and the workflow is aborted — same observable behavior as a manual reject.

Step 4 — Next gate sees allow, idempotency_key preserved

After the reviewer approves, the next /gate call goes through. Pass retry_policy: "reevaluate" so the policy engine re-runs against the freshly-approved step and returns allow:

from axonflow.workflow import RetryPolicy

gate = await client.step_gate(
workflow_id=workflow.workflow_id,
step_id="submit-pa",
request=StepGateRequest(
step_name="Submit prior authorization",
step_type=StepType.TOOL_CALL,
idempotency_key=IDEMPOTENCY_KEY,
retry_policy=RetryPolicy.REEVALUATE,
),
)

# gate.decision == "allow"
# gate.retry_context.idempotency_key == "pa:P-88120:J0135" (still)
Why retry_policy: "reevaluate"

Default-idempotent retries return the cached decision without consulting the policy engine. Right after an approval, the cached decision on the database row is still require_approval — the step was evaluated once and memoized. Passing retry_policy: "reevaluate" forces a fresh policy evaluation that sees the approved state and returns allow. This matches the pattern documented in Execution Boundary Semantics.

Step 5 — Submit to the payer, /complete with matching key

payer_response = payer_client.submit_prior_auth(
patient_id="P-88120",
cpt_code="J0135",
justification=clinical_note,
idempotency_key=IDEMPOTENCY_KEY, # same key across every layer
)

await client.mark_step_completed(
workflow_id=workflow.workflow_id,
step_id="submit-pa",
request=MarkStepCompletedRequest(
output={
"pa_reference": payer_response.reference,
"approved_by": "[email protected]",
"submitted_at": payer_response.submitted_at.isoformat(),
},
idempotency_key=IDEMPOTENCY_KEY,
),
)

await client.complete_workflow(workflow.workflow_id)

Using the same idempotency key at the payer layer is doubled protection — if the payer's response also times out, the next gate retry will still see prior_completion_status: "gated_not_completed" and the agent can ask the payer "did a PA request with key pa:P-88120:J0135 already land?" before re-submitting.

The safety story — approvals cannot be retargeted

Imagine a bug in your agent's record-handling code causes a state mix-up: the agent starts acting on patient P-91234 / procedure J9999 but tries to complete the submit-pa step from patient P-88120's workflow. Without idempotency discipline, that agent would submit the wrong procedure under an approval that was granted for a different patient. With idempotency_key pinned on both ends:

from axonflow.exceptions import IdempotencyKeyMismatchError

try:
await client.mark_step_completed(
workflow_id=workflow.workflow_id,
step_id="submit-pa",
request=MarkStepCompletedRequest(
output={"pa_reference": "PA-WRONG"},
idempotency_key="pa:P-91234:J9999", # wrong patient + procedure
),
)
except IdempotencyKeyMismatchError as e:
# Refuses to complete. e.expected_idempotency_key == "pa:P-88120:J0135"
# e.received_idempotency_key == "pa:P-91234:J9999"
alert_clinical_safety_team(e)

The call fails with 409 IDEMPOTENCY_KEY_MISMATCH before the PA submission, the audit log records the attempted mismatch, and the bug surfaces as a clear alert instead of a miscategorized claim submission.

Evaluation-tier extension — retry-aware escalation

Core flow above uses only the standard HITL queue. The Evaluation-tier extension is a retry-aware policy that layers additional governance on top: if an agent spends too long cycling through retries on a gated_not_completed step, escalate severity or require a second approver.

curl -X POST http://localhost:8080/api/v1/policies \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'pa-agent:your-secret' | base64)" \
-d '{
"name": "stuck-pa-escalates-severity",
"description": "PA requests stuck in gated_not_completed for 30+ minutes escalate to critical severity",
"type": "context_aware",
"category": "dynamic-compliance",
"priority": 900,
"enabled": true,
"conditions": [
{"field": "step.prior_completion_status", "operator": "equals", "value": "gated_not_completed"},
{"field": "step.first_attempt_age_seconds", "operator": "greater_than", "value": 1800}
],
"actions": [
{"type": "require_approval", "config": {"reason": "Prior authorization stuck in uncertain state for 30+ minutes — escalating for clinical safety review", "severity": "critical"}}
]
}'

Paired with retry_policy: "reevaluate" on polling gate calls, this policy upgrades a half-hour-stuck PA request to severity: "critical", which on Enterprise tier triggers SLA escalation and in-portal highlighting for the reviewer queue. On Evaluation tier it surfaces the escalation via the severity-filterable queue listing.

Any step.* condition on a Community license is rejected with 403 FEATURE_REQUIRES_EVALUATION_LICENSE at create time. See Retry Semantics & Idempotency — Retry-aware policies for the full rejection envelope and the list of step.* fields.

What this protects you from

  1. Approved decisions being retargeted to a different patient/procedure — the key mismatch check refuses before the payer ever sees the wrong request
  2. Duplicate PA submissions on orchestrator retryprior_completion_status tells the agent whether the payer already received the submission; reconcile with the payer's own idempotency store before re-submitting
  3. Lost ack mid-submission — same reconciliation pattern as Payment Agent — Retry & Reconciliation
  4. Stuck approvals consuming reviewer attention silently — the Evaluation-tier retry-aware policy above escalates stuck PAs rather than letting them age out invisibly

What this does not cover

  • Cross-workflow replay — if a workflow expires (24h on Evaluation) and your application creates a new one for the same patient + procedure, AxonFlow does not enforce key match across workflow IDs. Keep your own idempotency store on the PA reference and check it before creating the replacement workflow. Cross-workflow enforcement is a planned future enhancement.
  • Clinical-record identity checks — AxonFlow enforces key-level identity on the workflow step. Patient identity verification against the EHR is your application's responsibility.
  • PHI redaction in audit logs — use AxonFlow's PII detection to ensure idempotency keys and step inputs don't carry raw PHI.