Workflow triggers (draft)
Reference for agent-workflow trigger types and webhook signing
Triggers
Agent workflows can be triggered by manual dispatch, cron schedules, integration events, or webhooks.
Webhook triggers
Webhook triggers expose a public URL that any HTTP client can POST to. To prevent anyone with the URL from firing your workflow, every request must carry a valid HMAC-SHA256 signature derived from a per-trigger secret.
Headers
| Header | Value |
|---|---|
X-Helios-Timestamp | Unix seconds at the moment of signing |
X-Helios-Signature | sha256=<hex digest> |
Algorithm
- Capture the raw request body as bytes.
- Compute
HMAC-SHA256(secret, "${timestamp}.${rawBody}")and hex-encode it. - Send the request with both headers above.
The server rejects when:
- the signature header is missing or doesn't begin with
sha256=, - the timestamp is more than 5 minutes off the server's clock,
- or the computed digest doesn't match.
All failures return 401. The body is intentionally generic — server-side logs carry the precise reason.
Examples
import crypto from "node:crypto";
const url = "<your webhook url>";
const secret = "<your signing secret>";
const ts = Math.floor(Date.now() / 1000);
const body = JSON.stringify({ hello: "world" });
const sig = crypto
.createHmac("sha256", secret)
.update(`${ts}.${body}`)
.digest("hex");
await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Helios-Timestamp": String(ts),
"X-Helios-Signature": `sha256=${sig}`,
},
body,
});ts=$(date +%s)
body='{"hello":"world"}'
sig=$(printf '%s.%s' "$ts" "$body" \
| openssl dgst -sha256 -hmac "$SECRET" -hex \
| awk '{print $2}')
curl -X POST "$URL" \
-H "X-Helios-Timestamp: $ts" \
-H "X-Helios-Signature: sha256=$sig" \
-H 'Content-Type: application/json' \
--data "$body"Rotating the secret
You can rotate a webhook trigger's signing secret from the workflow's trigger panel. Rotation immediately invalidates the previous secret — every existing sender will start receiving 401 until reconfigured with the new value. There is no overlap window.
Notes
- The secret is treated as a UTF-8 string, not raw bytes. Both ends sign the same way; don't
Buffer.from(secret, "hex")on one side only. - The body is signed exactly as transmitted. If a proxy mutates whitespace or re-encodes JSON, signatures will fail.
- Empty bodies are signed as the empty string — sign over
${ts}.(no body bytes).
Last updated on