Skip to content
AGH RuntimeAutomation

Webhooks

Set up signed AGH webhook triggers for CI, deployment, and external event integrations.

Audience
Operators running durable agent work
Focus
Automation guidance shaped for scanability, day-two clarity, and operator context.

Webhooks are the external ingress path for automation. They accept signed HTTP requests, normalize the request into a webhook activation envelope, and dispatch every enabled webhook trigger that matches the endpoint, scope, workspace, and filters.

There is no separate webhook CLI group. Webhooks are configured as triggers with event = "webhook".

Routes

Webhook endpoint paths include the endpoint slug and webhook ID in one segment:

<endpoint_slug>--<webhook_id>

The webhook ID starts with wbh_.

ScopeRoute
GlobalPOST /api/webhooks/global/<endpoint_slug>--<webhook_id>
WorkspacePOST /api/webhooks/workspaces/<workspace_id>/<endpoint_slug>--<webhook_id>

AGH returns a result object with the number of matched triggers and the runs created from the delivery.

{
  "result": {
    "matched": 1,
    "runs": [
      {
        "id": "run_...",
        "trigger_id": "trg_...",
        "status": "scheduled"
      }
    ]
  }
}

Configure a webhook trigger

Config-backed webhook triggers need an endpoint slug and a webhook_secret_ref. Use env:NAME for operator-managed environment variables or a vault:automation/... ref for AGH-managed encrypted secret storage. AGH resolves the secret only when validating signed webhook requests. Store AGH-managed values with agh vault put, Settings > Vault, or the write-only API before enabling the trigger.

[[automation.triggers]]
scope = "workspace"
workspace = "/Users/you/src/checkout-api"
name = "deploy-webhook"
event = "webhook"
endpoint_slug = "deploy"
webhook_secret_ref = "env:AGH_DEPLOY_WEBHOOK_SECRET"
agent = "deployer"
prompt = "Validate deployment {{ index .Data \"sha\" }} on {{ index .Data \"branch\" }}."
filter = { "data.action" = "deploy", "data.branch" = "main" }
retry = { strategy = "backoff", max_retries = 2, base_delay = "5s" }
fire_limit = { max = 6, window = "1h" }

Dynamic webhook triggers can be created with the CLI by passing the secret directly. The secret is write-only through the API surface: AGH uses it for signature validation, but it is not returned by read endpoints.

agh automation triggers create \
  --name deploy-webhook \
  --scope workspace \
  --workspace /Users/you/src/checkout-api \
  --event webhook \
  --endpoint-slug deploy \
  --webhook-secret "$AGH_DEPLOY_WEBHOOK_SECRET" \
  --agent deployer \
  --prompt 'Validate deployment {{ index .Data "sha" }} on {{ index .Data "branch" }}.' \
  --filter data.action=deploy,data.branch=main \
  --retry backoff:2:5s

Read the trigger to get its webhook path:

agh automation triggers get <trigger-id>

Authentication

Every webhook request must include these headers:

HeaderRequiredMeaning
X-AGH-Webhook-TimestampYesUnix seconds or RFC3339 timestamp. Must be within the freshness window.
X-AGH-Webhook-SignatureYessha256= followed by the HMAC-SHA256 digest.
X-AGH-Webhook-Delivery-IDYesIdempotency key. Replays are rejected while the delivery ID is inside the freshness window.

The freshness window is 5m. Requests older than the window, too far in the future, or signed with the wrong secret are rejected.

The signature message is:

<unix_timestamp>.<raw_request_body>

Use Unix seconds for the timestamp header unless your integration has a reason to send RFC3339. When AGH receives RFC3339, it parses the timestamp and validates the signature against the parsed Unix seconds value.

Payload format

Request bodies are limited to 1MiB.

If the body is a JSON object, AGH exposes its fields under .Data for filters and prompt templates. If the JSON object does not already include payload, AGH also adds the raw request body as .Data.payload.

AGH adds webhook metadata to every delivery:

Data keyMeaning
endpointFull endpoint segment.
endpoint_slugHuman-readable slug from the endpoint.
webhook_idStable webhook ID.
timestampParsed delivery timestamp.

If the body is empty or not a JSON object, AGH exposes the raw body as .Data.payload alongside the metadata fields.

Send a signed request

This example posts a deployment payload to a workspace webhook. It uses openssl to calculate the HMAC signature.

body='{"action":"deploy","branch":"main","repository":"checkout-api","sha":"abc123"}'
timestamp="$(date -u +%s)"
signature="sha256=$(printf '%s.%s' "$timestamp" "$body" | openssl dgst -sha256 -hmac "$AGH_DEPLOY_WEBHOOK_SECRET" -hex | awk '{print $2}')"

curl -sS -X POST "http://localhost:2123/api/webhooks/workspaces/ws_123/deploy--wbh_abc123" \
  -H "Content-Type: application/json" \
  -H "X-AGH-Webhook-Timestamp: $timestamp" \
  -H "X-AGH-Webhook-Signature: $signature" \
  -H "X-AGH-Webhook-Delivery-ID: deploy-$timestamp" \
  --data "$body"

With the trigger above, AGH only dispatches when both filters match:

  • data.action = deploy
  • data.branch = main

Worked example: CI deployment review

This GitHub Actions step notifies AGH after a deployment workflow reaches the point where an agent should validate the deployment request.

name: Notify AGH deployment automation

on:
  workflow_dispatch:

jobs:
  notify-agh:
    runs-on: ubuntu-latest
    steps:
      - name: Send signed deployment webhook
        env:
          AGH_WEBHOOK_URL: ${{ secrets.AGH_WEBHOOK_URL }}
          AGH_WEBHOOK_SECRET: ${{ secrets.AGH_WEBHOOK_SECRET }}
          BRANCH: ${{ github.ref_name }}
          REPOSITORY: ${{ github.repository }}
          SHA: ${{ github.sha }}
        run: |
          body=$(jq -nc \
            --arg action deploy \
            --arg branch "$BRANCH" \
            --arg repository "$REPOSITORY" \
            --arg sha "$SHA" \
            '{action:$action, branch:$branch, repository:$repository, sha:$sha}')

          timestamp=$(date -u +%s)
          signature="sha256=$(printf '%s.%s' "$timestamp" "$body" | openssl dgst -sha256 -hmac "$AGH_WEBHOOK_SECRET" -hex | awk '{print $2}')"

          curl -sS -X POST "$AGH_WEBHOOK_URL" \
            -H "Content-Type: application/json" \
            -H "X-AGH-Webhook-Timestamp: $timestamp" \
            -H "X-AGH-Webhook-Signature: $signature" \
            -H "X-AGH-Webhook-Delivery-ID: deploy-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
            --data "$body"

Store the full webhook URL in AGH_WEBHOOK_URL, for example:

http://localhost:2123/api/webhooks/workspaces/ws_123/deploy--wbh_abc123

Errors

StatusWhen AGH returns it
400Invalid endpoint shape, missing required webhook headers, oversized body, or missing webhook secret configuration.
401Stale timestamp, future timestamp, or invalid webhook signature.
404No trigger registration matches the route.
409Replay delivery ID, fire limit, duplicate registration, or read-only definition conflict.
503Automation manager is not running.

Monitoring deliveries

List failed webhook-triggered runs:

agh automation runs --status failed --last 20

Read one run:

agh automation runs get <run-id>

Use trigger history when you know which webhook trigger owns the endpoint:

agh automation triggers history <trigger-id> --last 20

On this page