HeliosHELIOS DOCS
Agent workflows

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

HeaderValue
X-Helios-TimestampUnix seconds at the moment of signing
X-Helios-Signaturesha256=<hex digest>

Algorithm

  1. Capture the raw request body as bytes.
  2. Compute HMAC-SHA256(secret, "${timestamp}.${rawBody}") and hex-encode it.
  3. 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