Triggers
Give the runtime the clock, instead of holding your own scheduler. A trigger runs an agent or an orchestration on a cron, on demand, or both — and records every run.
Overview
Before triggers, the only way to run an agent was an inbound call from a caller holding its own scheduler. A trigger lets naturali hold it instead: create one against a cron expression and a target, and it fires on schedule from then on. Every fire — scheduled or manual — is recorded as a firing: what ran, when, and what came back.
A trigger runs one of two things, chosen with target_type:
- an agent (
target_type: agent, the default) — the common case, "run this agent daily at 9am UTC", and the building block behind naturali's solo-agent pattern: agent + cron, a single-role automation with one cycle; - an orchestration (
target_type: orchestration) — the scheduled counterpart of starting a run by hand. Each fire starts a run of the graph, and the trigger'sinputseeds that run's input.
Reach for an orchestration target when the scheduled work is more than one step — a nightly pipeline that queries knowledge, runs an agent over each result and pauses for a human on anything unusual. Reach for an agent target when one generation is the whole job.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Triggers.
Data Model
Trigger
| Field | Type | Description |
|---|---|---|
id | string | Public trigger ID (trg_ prefix) — the runtime trigger id. |
project_id | string | The owning project. |
name | string | |
description | string, nullable | |
type | "schedule" | Fixed — the only trigger type naturali fronts today. |
target_type | "agent" | "orchestration" | What the trigger runs. Defaults to agent on create. |
target_id | string | The agent or orchestration this trigger runs, per target_type. |
input | object, nullable | Static input passed to the target on every fire, shallow-merged under any fire-time input. For an orchestration target, this is the run's initial input. |
cron | string | 5-field cron expression, evaluated in UTC. |
active | boolean | Whether the trigger fires at all — on schedule and on a manual …:fire alike. |
next_fire_at | string (date-time), nullable | Server-computed; the next time the schedule fires. |
created_at | string (date-time) | |
updated_at | string (date-time) |
Firing
| Field | Type | Description |
|---|---|---|
id | string | Public firing ID — the runtime firing id. |
trigger_id | string | The trigger that ran. |
project_id | string | The owning project. |
source | "schedule" | "manual" | What caused this run. |
status | pending | running | succeeded | failed | |
input | object, nullable | The input this run actually used. |
result | object, nullable | { target_type, result_id, status, output } — output truncated. |
error | object, nullable | { code, message, meta }, present only when status is failed. |
started_at | string (date-time), nullable | |
completed_at | string (date-time), nullable | |
created_at | string (date-time) | |
updated_at | string (date-time) |
Key Concepts
One type, two targets — and what is still not fronted
The runtime's own trigger primitive also has manual/webhook types and a
tool target, which naturali doesn't front. A webhook trigger has no
naturali-side receiver to front it with; a tool target also needs an action
naming which operation of the tool to call, which is a surface of its own
rather than one more enum value. type is still on the wire, fixed to
"schedule", rather than omitted: a client reading it today keeps reading it
once a second value exists.
A trigger of a shape this surface doesn't front — created directly against
the runtime, bypassing naturali — reads as 404 here, the same treatment
traces gives a trace's file_id: the contract only speaks
for what it fronts.
Retargeting: the kind and the id move together
target_id on its own retargets within the trigger's current kind, and the new
id is validated against that kind — so retargeting an orchestration trigger
never resolves the new id as an agent.
Changing target_type requires sending target_id alongside it, or the call is
400. The two are one fact: accepting the kind alone would leave the trigger
pointing an orchestration target at an agent id.
Listing both kinds, and what total counts
GET /v1/projects/{project_id}/triggers
returns both fronted kinds. Pass target_type to narrow to one — that filter is
applied upstream, so total is exact.
Without it, total is the count of every schedule trigger in the project. It
can therefore exceed the rows returned, but only when a trigger was authored
directly against the runtime with a target this surface doesn't front — the same
bypass the 404 above covers, and not something this API can produce. Pass
target_type when an exact count matters.
Firing an inactive trigger is 409
active: false stops the schedule and blocks a manual …:fire call — both
routes go through the same guard, so "off" means off regardless of how the
run was requested. Flip active back to true to resume either path.
input layers, it doesn't replace
A trigger's stored input is the baseline for every fire. A manual
…:fire call's input is shallow-merged over it for that one run only — the
trigger's own configuration is unchanged, so a one-off override never drifts
the schedule's defaults.
Examples
Schedule an agent
- CLI
- SDK
- curl
naturali create-trigger \
--project-id proj_V1StGXR8Z5jdHi6B \
--name health-check \
--target-id agent_V1StGXR8Z5jdHi6B \
--cron "0 9 * * *"
const { data: trigger } = await naturali.triggers.createTrigger({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
name: 'health-check',
target_id: 'agent_V1StGXR8Z5jdHi6B',
cron: '0 9 * * *',
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/triggers \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "health-check",
"target_id": "agent_V1StGXR8Z5jdHi6B",
"cron": "0 9 * * *"
}'
Schedule an orchestration
input becomes the run's initial input on every fire, so the graph's
input_schema sees { "window": "24h" } here exactly as it would from
POST /v1/projects/{project_id}/orchestration-runs.
- CLI
- SDK
- curl
naturali create-trigger \
--project-id proj_V1StGXR8Z5jdHi6B \
--name nightly-digest \
--target-type orchestration \
--target-id orch_V1StGXR8Z5jdHi6B \
--cron "0 3 * * *" \
--input '{"window":"24h"}'
const { data: trigger } = await naturali.triggers.createTrigger({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
name: 'nightly-digest',
target_type: 'orchestration',
target_id: 'orch_V1StGXR8Z5jdHi6B',
cron: '0 3 * * *',
input: { window: '24h' },
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/triggers \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly-digest",
"target_type": "orchestration",
"target_id": "orch_V1StGXR8Z5jdHi6B",
"cron": "0 3 * * *",
"input": { "window": "24h" }
}'
Each firing's result then names the run it started —
{ "target_type": "orchestration", "result_id": "orch_run_…" } — which is the
id to read with
GET /v1/projects/{project_id}/orchestration-runs/{orchestration_run_id}.
Fire it now, and read the firing back
- CLI
- SDK
- curl
naturali fire-trigger \
--project-id proj_V1StGXR8Z5jdHi6B \
--trigger-id trg_V1StGXR8Z5jdHi6B
naturali list-trigger-firings \
--project-id proj_V1StGXR8Z5jdHi6B \
--trigger-id trg_V1StGXR8Z5jdHi6B
const { data: firing } = await naturali.triggers.fireTrigger({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
trigger_id: 'trg_V1StGXR8Z5jdHi6B',
},
});
const { data: firings } = await naturali.triggers.listTriggerFirings({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
trigger_id: 'trg_V1StGXR8Z5jdHi6B',
},
});
curl -X POST "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/triggers/trg_V1StGXR8Z5jdHi6B:fire" \
-H "Authorization: Bearer $NATURALI_API_KEY"
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/triggers/trg_V1StGXR8Z5jdHi6B/firings" \
-H "Authorization: Bearer $NATURALI_API_KEY"
Pause a schedule, then retune it
- CLI
- SDK
- curl
naturali update-trigger \
--project-id proj_V1StGXR8Z5jdHi6B \
--trigger-id trg_V1StGXR8Z5jdHi6B \
--active false
naturali update-trigger \
--project-id proj_V1StGXR8Z5jdHi6B \
--trigger-id trg_V1StGXR8Z5jdHi6B \
--cron "0 */6 * * *" \
--active true
await naturali.triggers.updateTrigger({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
trigger_id: 'trg_V1StGXR8Z5jdHi6B',
},
body: { active: false },
});
await naturali.triggers.updateTrigger({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
trigger_id: 'trg_V1StGXR8Z5jdHi6B',
},
body: { cron: '0 */6 * * *', active: true },
});
curl -X PATCH https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/triggers/trg_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "active": false }'
curl -X PATCH https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/triggers/trg_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "cron": "0 */6 * * *", "active": true }'