Turn a business event into a run: create a workflow 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 waiting run with POST /v1/workflows/runs/:runId/events; the payload becomes available to the following nodes.
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" }
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).
PUT/v1/workflows/:id/versions/draftReplace the draft graph (creates the next version when the current one is published).