webhook on a run or a cron and Aegra POSTs the finished run to that URL once it reaches a terminal status. The parameter, the payload, and the webhooks config block follow LangGraph Platform, so receivers and langgraph.json settings carry over unchanged.
Set a webhook
Passwebhook when you create a run. It works on runs.create, runs.stream, and runs.wait, threaded or stateless:
422 before anything is created. See URL policy.
When it fires
Each run schedules one webhook delivery, after its final status is saved:
Whoever saves the final status schedules the delivery, so two processes never both report the same run. Delivery itself is best effort: it can be dropped, and a retry after a timeout can reach a receiver that already handled the first attempt, so deduplicate by
run_id. A run that a worker hands back (lease loss, shutdown drain) fires when it finishes on the worker that completes it. A run cancelled while it executes reports the state it reached and its run_started_at. A run cancelled before it started, or whose worker stopped renewing its lease, fires interrupted from the cancel request with values and run_started_at set to null.
Deleting a thread, or a run with force=1, cancels its active runs first, so each of them fires interrupted before its row is removed. When the run executes in another server process, that webhook comes from the delete request with values and run_started_at set to null.
Deleting an assistant removes its runs without cancelling them, so an assistant’s runs that are still active when it is deleted send no webhook. Cancel them first if you need their results.
Payload
The body is the run in LangGraph’s webhook format:
To send fewer fields, list the ones to keep in
webhooks.allowed_fields:
Delivery
POSTwithContent-Type: application/jsonand any static headers.- Up to 3 attempts. Connection errors, DNS failures, timeouts,
429, and5xxare retried after about 1 and 2 seconds. Other statuses are not retried. - When the host resolves to several addresses, each attempt tries them in DNS order until one accepts the connection, so an IPv6 address on a host without IPv6 routing does not fail the delivery.
- Redirects are not followed: a
3xxanswer counts as a failed delivery. - Each attempt has a 5 second connect timeout and a 30 second overall timeout.
- Delivery runs in the background, so the run never waits on your receiver. At shutdown, deliveries still in flight get 5 seconds before they are dropped.
- At most 1,000 deliveries wait per server process. Beyond that, new ones are dropped and logged, so a slow receiver cannot grow memory without bound.
Secure the receiver
Your receiver should check that a call comes from your Aegra server. Two ways:- Token in the URL. Put a secret in the query string, for example
?token=<secret>. Aegra’s logs show only the scheme, host, and path of a webhook URL. - Static headers. Set
webhooks.headersinaegra.json. Values can reference environment variables with${{ env.NAME }}:
webhooks.env_prefix (default LG_WEBHOOK_) can be referenced, so a config file cannot read arbitrary secrets from the environment. Templates are resolved at startup: a missing variable stops the server from booting.
URL policy
webhooks.url limits which URLs runs and crons can register. Aegra applies it when the run or cron is created (a violation is a 422) and again before every call, so tightening the policy also covers crons created earlier: their calls are skipped and logged.
These targets are always blocked: link-local addresses and cloud metadata endpoints (
169.254.0.0/16, 100.100.100.200, fd00:ec2::254, metadata.google.internal), unspecified, multicast and reserved ranges, and host names ending in .svc.cluster.local.
An IPv6 address that carries an IPv4 address (IPv4-mapped ::ffff:0:0/96, NAT64 64:ff9b::/96, 6to4 2002::/16, or either Teredo endpoint in 2001::/32) is checked as that IPv4 address, so 2002:7f00:1::1 counts as loopback.
The last check is by name only. A short service name such as billing or billing.default resolves through the cluster’s search domains to a private address, so only disable_private_ips: true keeps webhooks away from services inside your network.
Aegra resolves the host, checks every address it resolves to, and connects only to addresses it checked. DNS cannot send the call somewhere else between the check and the connection.
Exact entries in allowed_domains are trusted: the address checks above are skipped for them. To call a service inside your cluster, list its exact name, for example "billing.default.svc.cluster.local". Wildcard entries only narrow which hosts are accepted; their addresses are still checked.
Relative webhook URLs such as
/my-route are rejected with 422. LangGraph Platform calls its own routes in-process for those. With Aegra, use an absolute URL instead, for example http://localhost:2026/my-route, and set disable_loopback to false.Receive webhooks
Any HTTP endpoint that accepts a JSON POST works. A minimal FastAPI receiver that checks the token from static headers:2xx quickly and do slow work elsewhere: each attempt waits at most 30 seconds. A run can arrive twice when a response is lost, so deduplicate on run_id.
A receiver can also be a custom route on the Aegra server itself. Calling the server’s own host is a loopback call, so it needs "webhooks": {"url": {"disable_loopback": false}}. Enable that only when you control every route the server exposes.
Disable webhooks
webhook get a 422, and runs fired by crons stored earlier skip the call.
Differences from LangGraph Platform
- Relative webhook URLs return
422instead of calling the server’s own routes. - With
http.disable_webhooks, a request that setswebhookgets a422instead of being accepted and never called. - Redirects are not followed.
- A run that times out has
status: "error", matching how Aegra records the run, witherror.errorset toTimeoutError.