Triggers API
Create, change and fire triggers: the per-agent way to start an agent on its own. A trigger is a cron schedule, a one-off time, a manual button or an incoming webhook (one type each), sends a prompt with optional variables, and creates an agent run. An agent can have several triggers, for example a schedule trigger and a webhook trigger. Concepts, reliability and examples are in the Triggers guide and the Webhook triggers guide; this page is the route reference.
All routes live under /api/client/v1, take Authorization: Bearer cpeer_… and need the agents service (GET read, everything else write). A token sees only its own project. Field names are snake_case; timestamps are unix seconds (except a once trigger's run_at, see below); errors use { "error": { "type", "message", "code" } }. The one exception to the token is the public hook endpoint, which senders call with the trigger's secret.
Endpoints
| Endpoint | |
|---|---|
GET /agents/{agentKey}/triggers | List an agent's triggers: { "object": "list", "data": […], "has_more": false } |
POST /agents/{agentKey}/triggers | Create |
GET /agents/{agentKey}/triggers/{triggerId} | Read |
PATCH /agents/{agentKey}/triggers/{triggerId} | Update |
DELETE /agents/{agentKey}/triggers/{triggerId} | Delete: { "id", "object": "agent.trigger.deleted", "deleted": true } |
POST /agents/{agentKey}/triggers/{triggerId}/run | Fire now (webhook triggers: with a sample payload, or a dry_run) |
POST /agents/{agentKey}/triggers/{triggerId}/webhook/rotate-secret | Rotate the webhook secret |
GET /agents/{agentKey}/triggers/{triggerId}/deliveries | The delivery log of a webhook trigger |
GET /agents/runs?trigger_id={triggerId} | History: the runs the trigger created |
POST /api/public/hooks/{tenantId}/{hookId} | Incoming webhook: no token |
The dashboard has the same set under /api/agents/{agentId}/triggers… (browser session, agents permission). It uses the same snake_case fields and the same error envelope as the client API; the only difference is that Run now takes no Idempotency-Key there.
The trigger object
{
"id": "6720b4c3d2e1f0a9b8c7d6e5",
"object": "agent.trigger",
"agent": "support-bot",
"name": "Morning ticket digest",
"description": null,
"enabled": true,
"type": "schedule",
"schedule": { "cron": "0 9 * * 1-5", "timezone": "Europe/Istanbul" },
"prompt": "Summarise the tickets opened since yesterday 09:00.",
"variables": null,
"version": null,
"class": "deferred",
"metadata": { "report": "morning-digest" },
"callback": { "url": "https://hooks.example.com/digest", "signed": true },
"response_format": null,
"next_fire_at": 1792389600,
"last_fire_at": 1792303201,
"last_run_id": "run_65f1c0a2b9d3e4f5a6b7c8d9",
"last_status": "started",
"last_error": null,
"created_at": 1791622800,
"updated_at": 1791622800
}| Field | |
|---|---|
type and its sub-object | What starts the trigger: see trigger types. One sub-object only, named after the type. |
prompt | The message each fire sends, up to 32 000 characters. For a webhook trigger it is a template. |
variables | Values for the agent's variables, or null. For a webhook trigger a mapping of variable names to values or "{{payload.…}}" tags. |
version | A published version number, or null for the published version at fire time. |
class | default or deferred. |
metadata | String pairs copied onto each run (at most 16 keys); {} when none. |
callback | { url, signed } or null. The secret is write-only. |
response_format | A JSON-schema answer format (as on runs) or null. |
next_fire_at | When the scheduler will fire next (unix seconds); null for manual and webhook triggers, disabled triggers and finished one-offs. |
last_fire_at, last_run_id | The latest fire and the run it created. |
last_status, last_error | started when the run was created, failed with the reason when it could not be (for example invalid variables: customer_id: Required). This is about creating the run; the run has its own status. |
created_at, updated_at | Unix seconds. |
Trigger types
A trigger has exactly one type, and the settings of that type sit in a sub-object of the same name. The sub-objects of the other types are not present.
type | Sub-object |
|---|---|
schedule | "schedule": { "cron": "0 9 * * 1-5", "timezone": "Europe/Istanbul" }. 5-field cron; timezone is an IANA name (default UTC). |
once | On write "once": { "run_at": "2026-10-18T06:00:00Z" } or "once": { "delay_seconds": 3600 }; run_at is ISO-8601 or unix seconds and must not be in the past. Read back, once.run_at is an ISO-8601 string. |
manual | None: "type": "manual" alone. |
webhook | See webhook trigger. |
Webhook trigger
{
"type": "webhook",
"webhook": {
"url": "https://console.example.com/api/public/hooks/65f1c0a2b9d3e4f5a6b7c8d9/wh_Zk3p0xH2m5Qe8TnV1aBcDe7Y",
"preset": "github",
"auth": "github_signature",
"header_name": null,
"secret_hint": "9F2c",
"secret_source": "generated",
"previous_secret_valid_until": null,
"events": ["pull_request"],
"filter": [ { "path": "payload.action", "op": "in", "value": ["opened", "synchronize"] } ],
"include_payload": true
}
}| Field | On write | On read |
|---|---|---|
preset | Required on create: github, gitlab, azure_devops or generic. | |
auth | One the preset offers (default: the first): github → github_signature; gitlab → gitlab_token, standard_webhooks; azure_devops → basic, header; generic → bearer, signature, header, standard_webhooks. | |
header_name | With auth: "header": the header that carries the secret, like x-my-token (default x-cognipeer-token). Names that carry signatures or routing are refused. | null unless auth is header. |
secret | Only with standard_webhooks: the sender's whsec_… secret (base64 that decodes to 24 to 64 bytes). Any other auth mode refuses it: the secret is generated. | Only in the answer that creates or replaces a generated secret, once; never after that. |
events | Up to 50 event names (A-Za-z0-9._:-, at most 64 characters); empty = all events. | |
filter | Up to 20 rules { path, op, value? }, all of which must match. op: eq, neq, in, not_in, exists, not_exists, contains, starts_with, glob. in and not_in take a list of 1 to 50 values; exists and not_exists take none. Reference. | |
include_payload | Boolean, default true: append the event JSON (up to 32 KiB) to the message. | |
url (the address a sender posts to), secret_hint (the last four characters), secret_source (generated or provided), previous_secret_valid_until (unix seconds while a rotated-out secret is still accepted, else null). |
The first time a secret is generated, which is on create, the answer carries it once, inside webhook:
{ "id": "6720b4c3d2e1f0a9b8c7d6e5", "object": "agent.trigger", "type": "webhook", "…": "…",
"webhook": { "url": "https://console.example.com/api/public/hooks/…/wh_…", "secret": "whsec_…", "secret_hint": "9F2c", "…": "…" } }The same webhook.secret appears on an update that generates a new secret (changing from standard_webhooks to another auth mode). It is never returned again.
A PATCH with a webhook sub-object keeps the hook id (so the URL) and the secret, with two exceptions: changing to standard_webhooks needs the sender's secret, and changing from it generates a new secret. A webhook sub-object sent back unchanged is not treated as a change. Changing type (together with the new type's sub-object) is allowed; a trigger that stops being a webhook stops answering at its URL (404).
Create
POST /api/client/v1/agents/{agentKey}/triggers| Field | Type | |
|---|---|---|
name | string | Required, up to 200 characters. |
description | string | Up to 2 000 characters. |
enabled | boolean | Default true. |
type | string | Required. schedule, once, manual or webhook. |
schedule, once, webhook | object | The settings of the chosen type: schedule for type: "schedule", and so on. manual has none. |
prompt | string | Up to 32 000 characters. Required unless the agent declares variables and variables is given. For a webhook trigger: a template of at most 16 KiB, required unless include_payload is on or variables is given. |
variables | object | Validated against the agent's declared variables; defaults apply at fire time. For a webhook trigger a mapping, checked at save: every name is declared, tags parse, other values have the declared type. |
version | number | null | Pin a published version. |
class | string | default (default) or deferred. |
metadata | object | Up to 16 string pairs. |
callback | { url, secret? } | Same rules as on a run: public http(s) URL, secret 16–256 characters. |
response_format | object | Optional structured-output format. |
curl -X POST "$CONSOLE_URL/api/client/v1/agents/support-bot/triggers" \
-H "Authorization: Bearer $COGNIPEER_API_TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Morning ticket digest",
"type": "schedule",
"schedule": { "cron": "0 9 * * 1-5", "timezone": "Europe/Istanbul" },
"prompt": "Summarise the tickets opened since yesterday 09:00.",
"class": "deferred"
}'201 with the trigger (and, for a webhook trigger, the one-time webhook.secret).
Errors: 400 invalid_trigger for any rejected field, with error.errors listing { "field", "message" } for each (schedule.cron, webhook.preset, webhook.secret, webhook.filter[0].op, prompt, variables.customer_id, …); 400 invalid_request when the body is not a JSON object and 400 invalid_json when it is not JSON; 404 agent_not_found.
An agent can have several triggers: call create again for each one.
Update
PATCH /api/client/v1/agents/{agentKey}/triggers/{triggerId}Accepts any field of create; omitted fields are untouched, null clears a nullable one. callback: null removes the callback; inside callback, an omitted secret or the mask keeps the stored secret and "" removes it. Changing the type or its settings, or enabled, recomputes next_fire_at; enabled: false stops scheduled fires and makes a webhook trigger answer 200 with accepted: false (manual run still works). 200 with the trigger; 404 trigger_not_found; 400 invalid_trigger as on create.
Delete
DELETE …/triggers/{triggerId} removes the trigger and its delivery log. Runs it created stay. 200 { "id", "object": "agent.trigger.deleted", "deleted": true }; 404 trigger_not_found.
Run now
POST /api/client/v1/agents/{agentKey}/triggers/{triggerId}/runFires the trigger immediately, whatever its type or enabled state. The body is optional:
| Field | |
|---|---|
variables | Replaces (merges over) the trigger's variables for this run. |
metadata | Merged over the trigger's metadata for this run. |
wait | Seconds to hold the request, as on the runs API. Prefer: wait=N works too. |
payload | Webhook triggers only: a sample event (a JSON object or array). |
headers | Webhook triggers only: sample headers (an object of string values, at most 50). |
dry_run | Webhook triggers only: true evaluates the sample and starts nothing. |
An Idempotency-Key header makes the call safe to retry (client API only). 200 with the run if it finished inside wait, 202 otherwise (with Location and Retry-After). The wait stops if the client disconnects; the run carries on. The run has trigger: "manual" and trigger_id set, and counts as the trigger's last fire. Its spend, run limits, quota and usage are counted against the caller and the API token that pressed Run now (the other fires count against the trigger's last updater, or its creator while nobody has changed it); the session stays owned by system:trigger:<id>.
With a webhook trigger. payload and headers are treated as a delivery would be, but they are data, never templates: the event name and delivery id are read from them the way the preset reads them, the prompt template and the variable mapping are rendered over them, and the run is built the way a delivery's is (external-data wording included). Events and Filter are not applied and nothing is de-duplicated. Without a payload the prompt is rendered over an empty event and nothing external is added.
With dry_run: true the answer is 200:
{
"object": "agent.trigger.dry_run",
"matched": false,
"reason": "rule 2 (payload.pull_request.draft neq) did not match (value: boolean)",
"event": "pull_request",
"delivery_id": null,
"prompt": "This run was started by an incoming webhook (github · pull_request). …",
"variables": null,
"errors": []
}matched says whether Events and Filter would let the delivery through (reason is why not); prompt and variables are what a run would get, or null with errors listing { field, message } when the template or the mapping cannot be applied.
| Status | Code | |
|---|---|---|
400 | invalid_variables, invalid_metadata, invalid_wait, invalid_request | A field was rejected. invalid_request also covers payload, headers or dry_run sent to a trigger that is not a webhook trigger. |
400 | invalid_payload, invalid_headers | The sample is not an object or array, or the headers are not an object of strings. |
400 | template_error, invalid_variables | The webhook turn cannot be built (error.errors has the fields). |
400 | agent_inactive | The agent is not active. |
404 | trigger_not_found, agent_not_found | |
409 | idempotency_key_conflict | The key belongs to a different request. |
429 | agent_run_concurrency_limit, spend_limit_exceeded, agent_quota_exceeded | Capacity, the spend limit, or the quota; with Retry-After. |
Rotate the webhook secret
POST /api/client/v1/agents/{agentKey}/triggers/{triggerId}/webhook/rotate-secret| Field | |
|---|---|
grace_seconds | A whole number from 0 to 86 400 (default 0): how long the current secret keeps working beside the new one. 0 also ends an earlier grace. |
secret | standard_webhooks only, and required there: the sender's new whsec_… secret. Refused for other auth modes. |
200:
{ "object": "agent.trigger.webhook_secret", "secret": "whsec_…", "secret_hint": "Qm4x", "previous_valid_until": 1792306800 }secret is present only for a generated secret, and only in this answer. Errors: 400 trigger_not_webhook when the trigger has another type, 400 invalid_trigger for a bad grace_seconds or secret, 404 trigger_not_found.
Deliveries
GET /api/client/v1/agents/{agentKey}/triggers/{triggerId}/deliveries?limit=20&before={id}The log of requests that reached the trigger's URL, newest first, kept for 7 days. limit is 1 to 100 (default 20; otherwise 400 invalid_limit); pass the last id as before for the next page while has_more is true.
{
"object": "list",
"has_more": false,
"data": [
{
"id": "6720b4c3d2e1f0a9b8c7d6f0",
"object": "agent.trigger.delivery",
"received_at": 1792303200,
"outcome": "accepted",
"http_status": 202,
"event": "pull_request",
"delivery_id": "72d3162e-cc78-11e3-81ab-4c9367dc0958",
"run_id": "run_65f1c0a2b9d3e4f5a6b7c8d9",
"reason": null,
"bytes": 8123
}
]
}| Field | |
|---|---|
outcome | accepted (a run started), duplicate, filtered, ignored (a ping, a test, a disabled trigger), unauthorized, rejected (a rate, capacity, spend or quota limit) or failed. |
http_status | What Console answered. |
event, delivery_id | As read from the delivery; null when not found or not valid. |
run_id | The run that started, or the one a duplicate repeats. |
reason | A short note of at most 300 characters, such as which filter rule did not match. It never holds payload text or a secret. |
bytes | The body size. |
Payloads are not stored. Rows other than accepted are sampled (up to 10 a minute per trigger on each node).
The public hook endpoint
POST /api/public/hooks/{tenantId}/{hookId}The URL a sender posts to is webhook.url of a webhook trigger. There is no API token: the tenantId and the random hookId (wh_…) locate the trigger, and every delivery proves the trigger's secret.
| Auth | The sender sends |
|---|---|
github_signature | X-Hub-Signature-256: sha256=<hex HMAC-SHA256 of the body> |
gitlab_token | X-Gitlab-Token: <secret> |
basic | Authorization: Basic base64(<any user>:<secret>) |
bearer | Authorization: Bearer <secret> |
header | <header_name>: <secret> |
signature | X-Cognipeer-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">, with t within 5 minutes of now |
standard_webhooks | webhook-id, webhook-timestamp (within 5 minutes), webhook-signature: v1,<base64 HMAC-SHA256 of "<id>.<timestamp>.<body>"> with the base64-decoded secret |
The body must be JSON (application/json or application/*+json, an object or an array, UTF-8, at most 64 levels deep) and at most AGENT_WEBHOOK_BODY_LIMIT_BYTES (default 5 MiB). Signatures are over the exact bytes received. The answers are tabulated in the guide: what Console answers. In short:
| Status | Answer |
|---|---|
202 | { "accepted": true, "run_id": "run_…", "status": "queued" }: a run was created. |
200 | { "ok": true, "event": "ping" | "test" } (a GitHub ping, an Azure DevOps test), or { "accepted": false, "reason": "disabled" | "event_not_selected" | "filtered" | "duplicate", "run_id"? }. |
200 | For gitlab and azure_devops, also { "accepted": false, "reason": "rate_limited" | "concurrency_limit" | "spend_limit" | "quota_exceeded" | "invalid_variables" | "template_error" | "internal_error", "message" } once a delivery reached the run step. |
400 | invalid_json; invalid_variables and template_error for github and generic. |
401 | webhook_unauthorized |
404 | webhook_not_found |
413 | payload_too_large |
415 | unsupported_media_type |
429 | webhook_rate_limited (the request ceiling, or the run rate); for github and generic also agent_run_concurrency_limit, spend_limit_exceeded and agent_quota_exceeded. Always with Retry-After. |
500 | internal_error (github, generic) |
503 | webhook_unavailable, with Retry-After: 30 |
A started run has trigger: "webhook", channel: "trigger" and trigger_id, carries the labels webhook_event and webhook_delivery in its metadata, and is unattended. Environment: AGENT_WEBHOOK_BODY_LIMIT_BYTES, AGENT_WEBHOOK_RATE_LIMIT_PER_MINUTE (operator settings).
Firing, misfires and exactly-once
The scheduler looks for due triggers every 30 seconds (AGENT_TRIGGER_TICK_MS). A fire claims its slot (trigger id and fire time) in the database and uses the run idempotency key trigger:<triggerId>:<fireTime>, so a trigger fires once even with several Console nodes. A slot missed by up to one hour (AGENT_TRIGGER_MISFIRE_GRACE_MS) fires once to catch up; an older slot is skipped. Each fire is a run in the trigger's class, with channel: "trigger" and trigger: "schedule", "once", "manual" or "webhook". A webhook delivery uses the key hook:<triggerId>:<hash> (de-duplication).
Events: trigger.fired and trigger.failed. trigger.failed is sent at most once an hour for each trigger and reason, so a schedule that is refused on every tick does not flood a subscriber.
Errors
| Status | Code | |
|---|---|---|
400 | invalid_trigger | A trigger field was rejected on create or update; error.errors lists { field, message }. |
400 | invalid_request, invalid_json, trigger_not_webhook, invalid_limit, and those under Run now | The body or a parameter is not valid. |
404 | agent_not_found, trigger_not_found | The agent or trigger does not exist in the token's project. |
409 | idempotency_key_conflict | On run-now only. |
429 | agent_run_concurrency_limit, spend_limit_exceeded, agent_quota_exceeded | On run-now: capacity, the spend limit, or the quota. |
SDK: client.agents.triggers.list() / create() / get() / update() / delete() / run() / rotateWebhookSecret() / deliveries() and signTriggerWebhook() (4.0.0+). See the SDK reference.

