Schedules, webhooks and callbacks

Schedules, webhooks and callbacks

An agent doesn't have to wait for a caller. A schedule runs it on a timer, a webhook runs it when another service calls a URL, and a callback sends the result of a run to your server when it finishes.

Schedules

Add one on the agent's Schedules tab, or:

lgr triggers create morning --agent support-bot \
  --schedule "0 8 * * 1-5" --message "Triage the tickets that came in overnight"
The Schedules tab of an agent with a weekday schedule and a webhook

Schedules are five-field cron expressions in UTC. Each time one fires, the agent's live version gets a new run in a fresh sandbox with the message, billed like any other run. A schedule missed while the platform was down fires once when it comes back.

Pausing the agent's API doesn't stop its schedules. Pause a schedule on its own on the Schedules tab, or lgr triggers disable morning (enable turns it back on). lgr triggers fire morning runs it now.

Webhooks

A webhook is a URL that starts a run when anything POSTs to it, such as a GitHub, Stripe or monitoring alert:

lgr triggers create alerts --agent oncall --webhook --message "New alert: {{body}}"

The URL is shown once, when the webhook is created. The token in it is the credential, so treat it like a key. {{body}} in the message is replaced with the request body, pretty-printed when it is JSON; without a message, the body is the message. Bodies of any content type up to 256 KB are accepted, and the answer is 202 with the run_id. A deleted or paused webhook answers 404.

To try one without the other service, send a sample body:

lgr triggers fire alerts --body '{"service":"checkout","error_rate":0.07}'

Callbacks

Give a run, message or trigger a callback URL and its result is POSTed there when the run finishes:

lgr triggers callback morning https://example.com/hooks/lagrange
lgr run --agent support-bot --callback https://example.com/hooks/lagrange "Summarise the open tickets"

The request looks like this:

POST /hooks/lagrange HTTP/1.1
content-type: application/json
user-agent: LagrangeCloud-Callbacks/1.0
lgr-delivery: dlv_3c9e01b2f7aa
lgr-event: run.completed
lgr-signature: t=1791403200,v1=5f1d…

{"id": "dlv_3c9e01b2f7aa", "type": "run.completed", "created_at": "2026-10-07T20:00:00.000Z", "run": { "id": "run_…", "status": "succeeded", "result": "…", "usage": { … }, "console_url": "…" }}

type is run.completed, run.failed or run.cancelled. Answer with any 2xx. Anything else, a redirect, or no answer within 10 seconds counts as a failure, and the same delivery is sent again after 30 seconds, 2 minutes, 10 minutes and 1 hour. Use lgr-delivery to drop duplicates.

Verifying the signature

lgr-signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, keyed with your project's signing secret (Settings → Callbacks, or lgr callbacks secret). Recompute it over the raw body, compare in constant time, and reject timestamps more than five minutes old:

import { createHmac, timingSafeEqual } from "node:crypto";

// header: the lgr-signature request header; body: the raw request body.
function verify(secret, header, body) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest();
  const given = Buffer.from(v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

Callback URLs must be http or https and must not point at private, loopback or link-local addresses. Recent deliveries are under Settings → Callbacks and lgr callbacks; lgr callbacks retry <id> sends one again and lgr callbacks rotate replaces the signing secret.

Instances and fleets

Triggers can also message an instance or fleet instead of starting a fresh run of an agent: use --instance NAME or --fleet NAME in place of --agent. On an instance, the message continues the conversation stored there.