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:
- Agent validates the request (budget check, policy compliance)
- Manager approval gate - with 1-hour timeout and 5-minute reminders
- Finance approval gate - with 4-hour timeout and 30-minute reminders
- 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
| Component | DIY | With AXME |
|---|---|---|
| State machine | 6 states, transitions, DB schema | Declarative in scenario file |
| Email notifications | SMTP setup, templates, bounce handling | Platform handles |
| Callback endpoints | Token generation, validation, routing | Not needed |
| Timeout/expiry | Cron job, DB queries | Configurable per gate |
| Reminders | Scheduler, dedup logic | Built-in (per gate) |
| Escalation | Custom routing, notification chain | Declarative |
| Audit trail | DB table, insert on every action | Automatic |
| Error recovery | Saga pattern, compensation logic | Intent 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.