AXME Cloud · 3 min read

A Two-Step Approval Chain Shouldn't Need a Workflow Engine

Manager approves, then finance approves. Simple enough to describe. 300 lines of code to build. Unless your platform handles approval chains natively.

Purchase request comes in. Manager approves. Then finance approves. Order goes through.

You can describe it in one sentence. Building it takes 300 lines of infrastructure code.

What “Simple Approval Chain” Actually Requires

# State machine
STATES = [
    "pending_manager",
    "approved_manager",
    "pending_finance",
    "approved",
    "rejected",
    "expired",
]

# Email service
def send_approval_email(approver, request, token):
    body = render_template("approval_email.html",
        request=request, token=token,
        approve_url=f"{BASE_URL}/approve/{token}",
        reject_url=f"{BASE_URL}/reject/{token}")
    smtp.send(approver.email, "Approval needed", body)

# Callback endpoint
@app.get("/approve/{token}")
def handle_approval(token):
    record = db.get("approval_tokens", token)
    if not record or record.expired:
        return "Link expired", 410
    if record.state == "pending_manager":
        db.update(record.id, state="approved_manager")
        # Now send to finance
        finance_token = generate_token()
        send_approval_email(finance_approver, record.request, finance_token)
        db.update(record.id, state="pending_finance")
    elif record.state == "pending_finance":
        db.update(record.id, state="approved")
        trigger_fulfillment(record.request)
    return "Approved", 200

# Timeout cron
@scheduler.cron("*/5 * * * *")
def expire_stale_approvals():
    stale = db.query(
        "SELECT * FROM approvals "
        "WHERE state IN ('pending_manager', 'pending_finance') "
        "AND created_at < NOW() - INTERVAL '48 hours'"
    )
    for record in stale:
        db.update(record.id, state="expired")
        notify_requester(record, "Your request expired")

Plus reminder emails. Plus audit logging. Plus token cleanup. Plus error handling when the DB write succeeds but the email fails. Plus bounce handling when the approver’s inbox is full.

”Just Use a Workflow Engine”

Sure. Here are your options:

Temporal - Deploy a cluster. Learn determinism constraints. Build the notification service. Build the approval UI. That’s a week before your first approval goes through.

AWS Step Functions - JSON state machine. Works if you’re all-in on AWS. Good luck reading the state definition a month later.

Airflow - Designed for batch data pipelines, not approval workflows. DAGs don’t wait for humans well.

n8n / Inngest - Closer, but human approval gates are limited to basic yes/no. No structured forms, no escalation, no reminder chains.

For a two-step approval, every option is either overkill or insufficient.

What This Looks Like With Intent-Based Approval

from axme import AxmeClient, AxmeClientConfig

client = AxmeClient(AxmeClientConfig(api_key=os.environ["AXME_API_KEY"]))

intent_id = client.send_intent({
    "intent_type": "purchase.request.v1",
    "to_agent": "agent://myorg/production/procurement",
    "payload": {
        "item": "ML GPU cluster (8x A100)",
        "amount": 284000,
        "requestor": "data-team",
        "justification": "Model training capacity for Q2",
    },
})
result = client.wait_for(intent_id)

The scenario file defines the chain:

  1. Agent validates the request (budget check, policy compliance)
  2. Manager approval gate - with 1-hour timeout and 5-minute reminders
  3. Finance approval gate - with 4-hour timeout and 30-minute reminders
  4. Processing and fulfillment

Each gate supports structured responses - not just approve/reject, but “approve with conditions”, “request more info”, “delegate to someone else”.

What You Stop Building

ComponentDIYWith AXME
State machine6 states, transitions, DB schemaDeclarative in scenario file
Email notificationsSMTP setup, templates, bounce handlingPlatform handles
Callback endpointsToken generation, validation, routingNot needed
Timeout/expiryCron job, DB queriesConfigurable per gate
RemindersScheduler, dedup logicBuilt-in (per gate)
EscalationCustom routing, notification chainDeclarative
Audit trailDB table, insert on every actionAutomatic
Error recoverySaga pattern, compensation logicIntent lifecycle

When a Workflow Engine Makes Sense

If your approval has 15 steps with conditional branching, compensation logic, and parallel execution paths - use Temporal or Step Functions. That’s what they’re built for.

But if your workflow is “person A approves, then person B approves, then it happens” - you don’t need a workflow engine. You need an approval chain with lifecycle guarantees.

Try It

Working example - purchase request with manager and finance approval gates, timeout, reminders, and full audit trail:

github.com/AxmeAI/approval-workflow-without-workflow-engine

Python, TypeScript, Go, Java, and .NET implementations included.

Built with AXME - approval workflows without the workflow engine. Alpha - feedback welcome.

More on AXME Cloud