Backend integration
How the demo portals do it
Meridian (accounts payable) and Dubai Police (complaint intake, escalation, gated case work) as reference implementations of the pattern.
The Agentic Workforce ME repository ships several demo portals that are built exactly the way this guide describes: a real application with its own database and UI, whose work is done by platform agents through the public API only. They are the reference architecture behind every page in this section. Two are worth reading closely.
Meridian Supplies — accounts payable
A small accounts-payable tool for a fictional trading company. Invoices arrive, are validated against purchase orders, wait for a manager’s approval, then get paid. Every one of those steps is performed by an agent; the portal has no local validation path at all (without a platform connection it refuses intake with 503 HIVE_NOT_CONFIGURED).
The loop
- Intake. A clerk submits an invoice. The server signs the JSON with the trigger secret (
x-hive-signature: sha256=…) and POSTs it to/hooks/:triggerId; the platform answers 202 with therun_id, which the portal stores on the invoice. (Triggers) - Validation. The
invoice-intakeworkflow runs theap-validatoragent, which reads the invoice, PO and vendor through the portal’s own MCP server (invoice.get,po.get,vendor.get) and writes its verdict back withinvoice.set_validationorinvoice.flag_exception— a visible state change in the portal’s database, attributed tohive:agent. (MCP) - Human decision. The workflow parks on a
wait_eventnode. When the AP manager approves or rejects in the portal, the server delivers the event withPOST /v1/workflows/runs/:runId/eventsand the run resumes. - Payment. On
run.completedfor an approved invoice, the portal starts theap-payment-clerkagent directly (POST /v1/agents/:id/runs). Itspayment.recordtool is gated by a high-risk approval policy: the run pauses until a Finance Controller decides in the console. The clerk is a direct run rather than a node in the intake workflow because a workflowagentnode cannot be resumed after an approval pause. (Approvals) - Closing the loop. The webhook receiver at
/api/hive/webhookverifiesHive-Signaturewith the SDK’sverifyWebhookSignature, de-duplicates on the envelopeid, acknowledges immediately and appliesrun.completed,run.failedandapproval.requestedto the invoice timeline asynchronously. (Webhooks)
What to copy
- Two files. Outbound (
server/hive/client.ts: trigger fire, SSE relay, event delivery, direct agent run) and inbound (server/hive/webhook.ts: verify, dedupe, apply). Everything platform-related is readable in one sitting. - The SDK is the only platform dependency. The portal may import
@hive/sdkand nothing else from the monorepo — the same position a real customer is in. - Evidence tables. Every outbound call and every inbound delivery is recorded (without secrets) and rendered on an “Integration” page that shows the signed payloads, the verification result and the dedupe hits. Build one; it turns debugging into reading.
- Honest failure. No silent fallback when the platform is unreachable: the UI says the processing is unavailable. Your users can cope with that; they cannot cope with fake results.
- Failure must not be fatal. Every route that calls the platform goes through an async wrapper and an error middleware, so an unreachable API becomes a
502 PLATFORM_UNAVAILABLEresponse, not a crashed process — Express 4 does not catch a rejected promise from anasynchandler, and Node exits on an unhandled rejection. The example’sasyncHandlerinserver.mjsis the whole pattern.
Dubai Police demonstration portal — intake, escalation, gates
A bilingual services portal for a fictional police tenant: citizens file reports, pay fines, object to fines and renew registrations; officers dispatch units and decide cases. Larger than Meridian, but the integration is the same shape with three additions worth studying.
| Pattern | How the portal does it |
|---|---|
| Multi-step workflow with a parked human step | report-intake: classify → enrich → propose a dispatch → wait_event dispatcher.decision. The dispatcher approves on the portal’s board; the server delivers the event and the run continues. Same for fine-dispute with committee.decision. |
| Gated write after the human step | The actual mutation (dispatch.commit, objection.decide, court.file_referral) is done by a separate direct agent run whose tool is covered by a high-risk approval policy — so the irreversible action always passes a platform-side approval, whatever the workflow decided. |
| Approvals decided inside the portal | POST /api/demo/approvals/:id/decision: the portal server checks its own session (command persona only), then calls POST /v1/approvals/:id/decision with its server-held service key and a feedback naming the persona. The key is an admin API key and the policies use approver_role: admin, so the decision is authorised and audited like a console decision. The SPA only ever sees the approval id. |
| Escalation from inside a tool | When the incident.classify MCP tool computes the “major crime” lane (from the agent’s indicators or a deterministic keyword floor the model cannot dodge), the portal itself moves the report to escalated, opens a linked case and fires a second signed trigger (dpf-major-case) — state changes on the portal’s own rules mid-run, never on model prose and never by waiting for a webhook. The receiving agent then reads the case through MCP (case.get, records.check, …) rather than trusting the prompt. |
| Webhooks as a timeline | approval.requested becomes a timeline entry (“awaiting the duty officer’s approval — nothing has moved yet”); run.completed for a record that did not change means the gate was rejected — the portal shows that honestly rather than assuming success. |
What to copy
- Separate deciding (a
wait_eventyour UI resumes) from committing (a gated tool under an approval policy). Your UI owns the business decision; the platform guarantees a human saw the exact action before it ran. - Proxy approvals only for roles that should decide, and record who clicked in your own audit as well as in
feedback. - Keep the local loop reachable: the portal’s public URL must be visible from the platform’s worker (
host.docker.internalplus the SSRF allow-list for a local stack; a tunnel otherwise).
The pattern, extracted
| Step | Meridian | Dubai Police | Guide page |
|---|---|---|---|
| Business event → run | Signed trigger (invoice submitted) | Signed trigger (report / objection filed) | Triggers |
| Agent acts on your data | MCP: invoice.*, po.get, vendor.get, payment.record | MCP: incident.*, dispatch.*, fines.*, objection.*, case.* | MCP |
| Human step mid-workflow | wait_event (manager approval) | wait_event (dispatcher / committee) | Workflows |
| Privileged action | payment.record under a high-risk policy | dispatch.commit, objection.decide, court.file_referral | Approvals |
| Decide from your UI | Console | Portal proxy with a server-held key | Proxy |
| Back to your system | Signed webhooks, deduped, applied async | Same | Webhooks |
| Provisioning | scripts/setup-hive.ts, slug-keyed, idempotent | Same, plus a reset script | Provisioning |
The runnable example in this portal (backend-integration-node) is a deliberately small version of the same shape: one Express process with the four surfaces and a provisioning script, so you can read all of it in ten minutes before opening the demos.
In the repository: demo/portal/ (Meridian), demo/dpf/ (Dubai Police), demo/mocd/ (a citizen-cases variant of the same loop), and the runbook docs/runbooks/dubai-police-demo.md.