Turn a business event into a run: create a workflow (or import a BPMN 2.0 diagram) and a webhook trigger, sign requests with HMAC (exact headers and canonical string), map payloads, test locally.
A trigger turns a business event into an Agentic Workforce ME run without an API key in the caller. You create it once, pointed at a published workflow or agent, and receive a secret. From then on, any system that can send an HTTPS POST with an HMAC signature — your monolith, a serverless function, a no-code tool — can start governed agent work. This is how the demo portals start every automated case.
Point a trigger at an agent when one agent should simply handle the event. Point it at a workflow when you want orchestration: several agents, branches, HTTP calls, waits for external events, human tasks. A workflow is a graph of typed nodes (start, agent, branch, http_request, mcp_tool, wait_event, human_task, parallel, foreach, subworkflow, end…); the trigger payload is available to every node as {{input}}.
POST /v1/workflowsJSON
{"slug":"order-escalation","name":"Order escalation","graph":{"nodes":[{"id":"start","type":"start","config":{}},{"id":"refunds","type":"agent","config":{"agent_id":"…","agent_version_id":"…",←thePUBLISHEDversionid(pinned)"input_template":"Handle this order escalation using your tools. Order {{input.order_id}}; reason: {{input.reason}}; idempotency_key: {{input.idempotency_key}}. Full event: {{input}}"}},{"id":"end","type":"end","config":{}}],"edges":[{"from":"start","to":"refunds"},{"from":"refunds","to":"end"}]}}
An agent node pins agent_version_id to a published version, so a later draft of the agent cannot change a running workflow until you republish the workflow too.
input_template is a Mustache-style string; {{input.x}} reads a field, {{input}} injects the whole event as JSON.
Keep agent nodes free of approval-gated tools: a child run that pauses for an approval fails the workflow (CHILD_RUN_FAILED). Put a gate node before a sensitive step instead, or run the gated agent directly — see Human-in-the-loop approvals.
The finished workflow run’s output is { outputs: { <node_id>: { text, child_run_id } }, variables }, not a bare { text } — read the node you care about when the run.completed webhook arrives.
Creating a workflow gives you draft v1; publish it before a trigger can start it (409 WORKFLOW_NOT_PUBLISHED otherwise). Later edits go to the draft (PUT …/versions/draft) and are published as a new version.
Shell
# Create returns { "workflow": { "id" } } with draft v1 → publish itcurl-X POST https://console.agenticworkforce.me/v1/workflows/$WORKFLOW_ID/versions/1/publish \-H "Authorization: Bearer $HIVE_API_KEY"-H "X-Tenant-Id: $HIVE_TENANT_ID"# Later changes: PUT the draft, publish the returned versioncurl-X PUT https://console.agenticworkforce.me/v1/workflows/$WORKFLOW_ID/versions/draft \-H "Authorization: Bearer $HIVE_API_KEY"-H "X-Tenant-Id: $HIVE_TENANT_ID"\-H "Content-Type: application/json"-d @graph.json
# → { "version": 2 }
You can also build the graph visually in the console (Workflows → New) and only script the trigger; the API and the console write the same resource.
POST /v1/triggers with kind: "webhook", the target and an optional config. The response includes the public hook path and, once only, the HMAC secret (64 hex characters). Store it with your secrets; if it is lost, POST /v1/triggers/:id/rotate-secret issues a new one and invalidates the old one immediately — there is no overlap window, so deploy the returned value straight away (or create a second trigger, cut over, then delete the old one).
By default the parsed JSON body is the run input. For an agent target the agent reads its prompt from text (or a bare string / input); for a workflow target, nodes address fields as {{input.field}}. If the sending system’s field names are fixed, add config.input_mapping to rename top-level fields on the way in:
The hook endpoint is POST /hooks/:triggerId on the API origin. It takes no Authorization or X-Tenant-Id header — the trigger id identifies the tenant and the signature proves the caller holds the secret.
import{ createHmac }from'node:crypto';/** x-hive-signature value for the exact bytes you send. */exportfunctionsignTriggerBody(secret:string,rawBody:string):string{return`sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;}exportasyncfunctionfireTrigger(payload: unknown):Promise<{run_id:string}>{const raw =JSON.stringify(payload);// sign THIS string, send THIS stringconst res =awaitfetch(`${process.env.HIVE_API_URL}/hooks/${process.env.HIVE_TRIGGER_ID}`,{method:'POST',headers:{'content-type':'application/json','x-hive-signature':signTriggerBody(process.env.HIVE_TRIGGER_SECRET!, raw),},body: raw,});if(res.status !==202)thrownewError(`hook ${res.status}: ${await res.text()}`);return(await res.json())as{run_id:string};}
fire_trigger.pyPython
import hashlib
import hmac
import json
import os
import urllib.request
defsign_trigger_body(secret: str,raw_body: bytes)->str:"""Value for the x-hive-signature header: sha256=<hex HMAC-SHA256(secret, raw body)>."""
digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()return"sha256="+ digest
deffire_trigger(payload: dict)->dict:
raw = json.dumps(payload, separators=(",",":")).encode("utf-8")# sign THESE bytes
req = urllib.request.Request(
f"{os.environ['HIVE_API_URL']}/hooks/{os.environ['HIVE_TRIGGER_ID']}",
data=raw,
method="POST",
headers={"Content-Type":"application/json","x-hive-signature":sign_trigger_body(os.environ["HIVE_TRIGGER_SECRET"], raw),},)with urllib.request.urlopen(req, timeout=30)asres:# 202 { "run_id": "…" }return json.loads(res.read())if __name__ =="__main__":print(fire_trigger({"event":"order.escalated","order_id":"ord_1001","reason":"Item arrived damaged","idempotency_key":"escalation:ord_1001:1"}))
// Java 17+ (HexFormat). Jackson for the body.import com.fasterxml.jackson.databind.ObjectMapper;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.nio.charset.StandardCharsets;import java.util.HexFormat;import java.util.Map;staticStringsignTriggerBody(String secret,byte[] rawBody)throwsException{Mac mac =Mac.getInstance("HmacSHA256");
mac.init(newSecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),"HmacSHA256"));return"sha256="+HexFormat.of().formatHex(mac.doFinal(rawBody));}byte[] raw =newObjectMapper().writeValueAsBytes(Map.of("event","order.escalated","order_id","ord_1001","reason","Item arrived damaged","idempotency_key","escalation:ord_1001:1"));// sign THESE bytesHttpRequest req =HttpRequest.newBuilder(URI.create(System.getenv("HIVE_API_URL")+"/hooks/"+System.getenv("HIVE_TRIGGER_ID"))).header("Content-Type","application/json").header("x-hive-signature",signTriggerBody(System.getenv("HIVE_TRIGGER_SECRET"), raw)).POST(HttpRequest.BodyPublishers.ofByteArray(raw)).build();HttpResponse<String> res =HttpClient.newHttpClient().send(req,HttpResponse.BodyHandlers.ofString());// 202
The Python sample and the example’s hive-signing.mjs are executed by this portal’s tests against the platform’s own verifier; the .NET and Java samples are checked for the same header, prefix and algorithm.
Serialise once, sign that string, send that string. Re-serialising (or letting an HTTP library re-encode the body) changes bytes and breaks the signature.
The comparison is constant-time on the full sha256=… string, so the prefix and lowercase hex are part of the contract.
An empty body is accepted and yields an empty input; a non-JSON body is a 400.
Put a unique idempotency_key (your record id plus a version or timestamp) in the payload and make the agent pass it to your mutating MCP tools, which must be idempotent on it.
Include occurred_at in the payload and let your tools reject stale events, or check the run’s created_at against it.
Only ever send hooks over HTTPS, rotate the secret when a system that held it is decommissioned, and keep one trigger per source system so a leak has a small blast radius.
Two authenticated shortcuts skip the HMAC while you develop: test-run on the workflow and fire on the trigger. Both take the same input shape the hook would deliver and return 202 { run_id }; follow the run with SSE or polling.
Shell
# Run the workflow without HMAC (same input shape the hook would deliver)curl-X POST https://console.agenticworkforce.me/v1/workflows/$WORKFLOW_ID/test-run \-H "Authorization: Bearer $HIVE_API_KEY"-H "X-Tenant-Id: $HIVE_TENANT_ID"\-H "Content-Type: application/json"\-d '{"input":{"event":"order.escalated","order_id":"ord_1001","reason":"test","idempotency_key":"test-1"}}'# Or fire the trigger itself through the authenticated API (no signature needed)curl-X POST https://console.agenticworkforce.me/v1/triggers/$HIVE_TRIGGER_ID/fire \-H "Authorization: Bearer $HIVE_API_KEY"-H "X-Tenant-Id: $HIVE_TENANT_ID"\-H "Content-Type: application/json"\-d '{"input":{"event":"order.escalated","order_id":"ord_1001","reason":"test","idempotency_key":"test-2"}}'
To exercise the real hook from a laptop, run the API locally or point your script at a staging tenant: the hook endpoint is reachable from anywhere, so no tunnel is needed in this direction (you need one for webhooks, where the platform calls you). GET /v1/runs/:id shows origin with the trigger_id, and the audit log records trigger.fire.
A workflow can park on a wait_event node until your system says so — a payment settled, a document was signed. Deliver the event to the run with POST /v1/workflows/runs/:runId/events; the payload becomes available to the following nodes. You do not have to wait for the run to reach the node: if it is still queued, running, or parked at an earlier gate, the event is buffered (202 { status: 'buffered' }) and consumed the moment the node is entered — so a re-fired run needs no retry loop on your side. Only a finished run refuses it (409 BAD_STATE).
Shell
# A run parked on a wait_event node resumes when your system delivers the eventcurl-X POST https://console.agenticworkforce.me/v1/workflows/runs/$RUN_ID/events \-H "Authorization: Bearer $HIVE_API_KEY"-H "X-Tenant-Id: $HIVE_TENANT_ID"\-H "Content-Type: application/json"\-d '{"event_name":"payment.settled","payload":{"settlement_id":"stl_88"}}'# → 202 { "run_id": "…", "event_name": "payment.settled", "status": "delivered" }# ("status": "buffered" when the run has not reached the node yet — no retry needed)
Teams that already model their processes in Camunda Modeler, bpmn.io, Signavio, Bizagi or Visio can import the BPMN 2.0 XML instead of redrawing it: POST /v1/workflows/import/bpmn (also Workflows → Import BPMN in the console). The importer is deterministic — the same file always yields the same draft — and produces an import report with one row per BPMN element: how it was mapped (mapped, approximated, placeholder, dropped, unsupported), a confidence, and a fix-it hint. Service, script, business-rule and send tasks become agent nodes that need a binding to one of your published agents (the Zeebe job type / Camunda topic is carried as the hint); Camunda HTTP connectors become http_request nodes; user tasks become gate or human_task; receive tasks and message events become wait_event nodes you resume with POST /v1/workflows/runs/:runId/events. Call it with dry_run: true first, bind the listed nodes, then commit — the result is always a new draft workflow, never a published one. Uploads are capped at 2 MiB and a <!DOCTYPE> is refused before parsing.
Audited as trigger.rotate_secret. Rotation invalidates the old secret immediately and there is no overlap window (the secret is generated server-side, so it cannot be deployed before it exists): rotate, then deploy the returned value straight away and expect a brief 401 window. For zero downtime, create a second trigger, cut your backend over, then delete the old one.
POST/v1/triggers/:id/fire202Fire a trigger now through the authenticated API (no HMAC).
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Exactly one start, at least one end; node types include agent, branch, mcp_tool, http_request, gate, human_task, wait_event, foreach, parallel, subworkflow.
Response
Field
Type
Description
workflow.id
uuid
Workflow id (the trigger target).
POST/v1/workflows/import/bpmn200 (dry run) · 201 (commit)Import a BPMN 2.0 diagram (Camunda / bpmn.io / Signavio / Bizagi / Visio export) into a NEW draft workflow, with a per-element import report. Deterministic; never publishes.
The BPMN 2.0 XML (≤ 2 MiB; <!DOCTYPE is refused). Alternatively send multipart/form-data with a file part and an optional body text part holding the same JSON without xml.
Which process to import; where bare guard identifiers resolve (variables | input); force every userTask to one node type; approver role when candidate groups give no signal; BPMNDI → canvas scale (1.6); serialise non-block parallel forks (true) or keep the first branch only.
slug · name · node_id · surface
string? · string? · uuid? · string?
New workflow identity (defaults derive from the process name) and optional department.
enrich
boolean?
Opt-in AI suggestions for bindings and lane → department mapping (one model call over names / documentation / hints — never the XML). Suggestions are post-filtered to the tenant’s published agents and org nodes and NEVER applied automatically; a model failure sets report.enrichment.status = "unavailable" and the request still succeeds. Default false.
create_triggers
boolean?
Commit only: create the suggested webhook / cron triggers DISABLED, targeting the new workflow.
Response
Field
Type
Description
draft
{ nodes, edges }
Dry run only — the mapped graph; unbound agent-like nodes carry empty ids until bound. Nodes carry meta (bpmn_id, name, description, lane, position, hints).
Dry run only — shapes and edges of the original diagram (BPMNDI coordinates).
workflow · version
object
Commit only — the new workflow and its draft version 1 (published_at: null), same shapes as POST /v1/workflows.
Mapping defaults: userTask → gate (approval keywords / managerial candidate groups) else human_task; service / script / business-rule / send tasks → agent (binding required; the Zeebe job type or Camunda topic is the hint) or http_request when a Camunda HTTP connector exposes a URL; receiveTask / message catch → wait_event; exclusive gateway → branch with single-comparison guards translated (FEEL / JUEL / JavaScript), untranslatable guards emitted as an obviously named never-true placeholder; a perfectly block-structured parallel fork → parallel, anything else serialised in XML order and reported approximated; gateway-headed cycles → loop (max 10); embedded sub-processes inlined; callActivity → subworkflow; timers → wait_event + sla_minutes (approximated).
Audit: commit writes workflow.import_bpmn (workflow_id, version_id, source_tool, sha256, bytes, counts, enrich — never the XML); workflow.import_enrich when the model was called. Dry runs persist nothing.
PUT/v1/workflows/:id/versions/draftReplace the draft graph (creates the next version when the current one is published).
POST/v1/workflows/runs/:runId/events202Deliver a named event to a workflow run. Parked at the matching wait_event node ⇒ the run resumes; not there yet (queued, running, or at an earlier gate) ⇒ the event is buffered and consumed the moment the node is entered — no retry loop needed.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)