An agent that runs database migrations, deploys configuration changes, or sends Slack announcements to a hundred engineers cannot execute those actions inside a stateless HTTP request handler. If the server restarts mid-execution, the agent loses its context, re-executes already-completed steps, or — worst case — applies a migration twice. The ephemeral request/response model is structurally incompatible with long-running autonomous work.
This article implements step-level durable execution for Workspace agent workflows: each completed step is checkpointed before the next begins, server restarts resume from the last checkpoint rather than from scratch, and destructive actions pause the entire workflow pending an explicit human authorisation in a React approval UI.
Who this is for: Engineers who have run multi-step AI agents inside HTTP handlers and hit timeouts, lost state on server restart, or accidentally repeated a step that should have been idempotent. Durable execution makes step replay safe and human approval gates auditable.
app.post('/agent/run', async (req, res) => {
const research = await agent.research(req.body.topic)
const draft = await agent.draft(research)
await agent.deploySchema(draft.schema)
res.json({ done: true })
})
import { inngest } from '../inngest'
import { FsmAgentRuntime } from '../agents/FsmAgentRuntime'
import { emitApprovalRequest } from '../events/ApprovalEvents'
export const researchAndDraftWorkflow = inngest.createFunction(
{
id: 'workspace-research-and-draft',
retries: 3,
concurrency: [{ limit: 10, key: 'event.data.orgId' }],
},
{ event: 'workspace/agent.run' },
async ({ event, step }) => {
const { orgId, userId, taskDescription, documentId } = event.data
const researchResults = await step.run('research-phase', async () => {
const agent = new FsmAgentRuntime({
maxSteps: 10,
maxTokensPerStep: 2048,
tools: [searchKnowledgeTool(orgId)],
systemPrompt: 'You are a research assistant. Search the knowledge vault and compile findings.',
requireHumanApproval: () => false,
})
return agent.run(`Research topic: ${taskDescription}`)
})
const draftContent = await step.run('draft-generation', async () => {
const agent = new FsmAgentRuntime({
maxSteps: 8,
maxTokensPerStep: 4096,
tools: [appendToDocumentTool(orgId, documentId)],
systemPrompt: 'You are a technical writer. Draft a document section based on research findings.',
requireHumanApproval: () => false,
})
return agent.run(`Draft based on research:\n${researchResults}`)
})
const schemaChangePayload = extractSchemaChanges(draftContent)
if (schemaChangePayload) {
await step.run('emit-approval-request', async () => {
await emitApprovalRequest({
workflowId: event.id,
orgId,
userId,
action: {
type: 'deploy_schema',
description: 'Deploy database schema changes derived from research',
payload: schemaChangePayload,
blastRadius: 'All tables in workspace database',
reversible: false,
},
})
})
const { data: decision } = await step.waitForEvent('await-human-approval', {
event: `workspace/approval.decided`,
match: 'data.workflowId',
timeout: '24h',
})
if (decision.approved !== true) {
return { status: 'rejected', reason: decision.reason }
}
await step.run('deploy-schema', async () => {
await deploySchemaChanges(orgId, schemaChangePayload)
})
}
return { status: 'completed', documentId }
},
)
The approval UI must show the exact blast radius of the proposed action — not a generic "approve?" dialog.
import { useState } from 'react'
import type { ApprovalRequest } from '@workspace/types'
interface Props {
request: ApprovalRequest
onDecide: (approved: boolean, reason?: string) => Promise<void>
}
export function ApprovalCard({ request, onDecide }: Props) {
const [loading, setLoading] = useState(false)
const [reason, setReason] = useState('')
const handleDecision = async (approved: boolean) => {
if (!approved && !reason.trim()) return
setLoading(true)
await onDecide(approved, approved ? undefined : reason)
setLoading(false)
}
return (
<div className="approval-card" role="dialog" aria-labelledby="approval-title">
<header className="approval-card__header">
<span className="approval-card__badge approval-card__badge--pending">
⏸ Awaiting Your Approval
</span>
<h2 id="approval-title">Agent Action Requires Authorisation</h2>
</header>
<section className="approval-card__action">
<h3>Proposed Action</h3>
<p>{request.action.description}</p>
</section>
{/* Blast radius — always shown for destructive actions */}
<section className="approval-card__blast-radius">
<h3>⚠ Blast Radius</h3>
<p>{request.action.blastRadius}</p>
<dl>
<dt>Reversible:</dt>
<dd className={request.action.reversible ? 'text-green' : 'text-red'}>
{request.action.reversible ? 'Yes' : 'No — this action cannot be undone'}
</dd>
</dl>
</section>
{/* Payload preview — diffs for schema, previews for content */}
<details className="approval-card__payload">
<summary>View full payload</summary>
<pre><code>{JSON.stringify(request.action.payload, null, 2)}</code></pre>
</details>
<footer className="approval-card__actions">
<div>
<label htmlFor="rejection-reason">Rejection reason (required to reject)</label>
<input
id="rejection-reason"
value={reason}
onChange={e => setReason(e.target.value)}
placeholder="Explain why this action should not proceed..."
disabled={loading}
/>
</div>
<div className="approval-card__buttons">
<button
className="button button--danger"
onClick={() => handleDecision(false)}
disabled={loading || !reason.trim()}
aria-label="Reject and stop this agent action"
>
Reject
</button>
<button
className="button button--primary"
onClick={() => handleDecision(true)}
disabled={loading}
aria-label="Approve and allow this agent action to proceed"
>
{loading ? 'Processing...' : 'Approve'}
</button>
</div>
</footer>
</div>
)
}
import { useSyncExternalStore } from 'react'
export function useApprovalRequests(orgId: string) {
const [requests, setRequests] = useState<ApprovalRequest[]>([])
useEffect(() => {
const controller = new AbortController()
async function subscribe() {
const response = await fetch(`/api/approvals/stream?orgId=${orgId}`, {
headers: { 'Accept': 'text/event-stream' },
signal: controller.signal,
})
for await (const event of fetchSSE(response)) {
const request = JSON.parse(event.data) as ApprovalRequest
setRequests(prev => [...prev.filter(r => r.id !== request.id), request])
}
}
subscribe().catch(console.error)
return () => controller.abort()
}, [orgId])
const decide = async (requestId: string, approved: boolean, reason?: string) => {
await fetch(`/api/approvals/${requestId}/decide`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ approved, reason }),
})
setRequests(prev => prev.filter(r => r.id !== requestId))
}
return { requests, decide }
}