Skip to main content

Run an agent on a schedule

By the end of this tutorial you will have an agent that runs on its own every morning — a schedule trigger pointed at it, proven by one fire you start by hand and a generation that names the trigger that started it.

Four steps:

  1. Create a schedule trigger that runs the agent.
  2. Fire it once by hand, so you don't wait for the schedule.
  3. Read the generation it produced.
  4. Read the firing log — where every scheduled run will show up from now on.

Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.

Prerequisites​

export NATURALI_TOKEN=nat_sk_...
export PROJECT=proj_...
export AGENT=agent_...
Your plan bounds triggers and how often they fire

An account holds at most 3 triggers on Free, 30 on Pro and 500 on Business, counted across all its projects. A schedule may fire at most hourly on Free, every 5 minutes on Pro and every minute on Business. Past either, the create answers 403 plan_limit_reached. Every firing is a generation, so each one counts as a run against the plan. See Triggers and Pricing.

1. Create a schedule trigger​

A trigger says what to run (target_type + target_id) and what starts it (type). For schedule, cron is a 5-field expression in UTC — 0 9 * * * is every day at 09:00, which every plan allows. For an agent target, input.message becomes the user message of each run.

naturali create-trigger \
--project-id "$PROJECT" \
--name morning-refund-reminder \
--type schedule \
--cron "0 9 * * *" \
--target-type agent \
--target-id "$AGENT" \
--input '{"message":"Write today'\''s one-line reminder of our refund window."}'
{
"id": "trg_KipkvLcSYfHYNBmA",
"project_id": "proj_cT9LACJi0WypPf5U",
"name": "morning-refund-reminder",
"type": "schedule",
"target_type": "agent",
"target_id": "agent_7VDoEY4pPnWYUyMV",
"input": {
"message": "Write today's one-line reminder of our refund window."
},
"cron": "0 9 * * *",
"active": true,
"next_fire_at": "2026-10-03T09:00:00.000Z"
}

next_fire_at is computed for you. The schedule is live from here: nothing else needs to run on your side.

export TRIGGER=trg_KipkvLcSYfHYNBmA

A cron tighter than your plan allows — */15 * * * * on Free — is refused before anything is created:

{
"error": {
"code": "plan_limit_reached",
"message": "The free plan schedules a trigger at most once every hour.",
"details": { "plan": "free", "resource": "trigger_interval", "limit": 3600 }
}
}

2. Fire it once by hand​

Waiting until 09:00 is a poor test. A fire runs the same target with the same input right now and answers with the finished firing.

naturali fire-trigger \
--project-id "$PROJECT" \
--trigger-id "$TRIGGER"
{
"id": "trg_fire_WCXDKoYaTTSPY2vm",
"trigger_id": "trg_KipkvLcSYfHYNBmA",
"source": "manual",
"status": "succeeded",
"input": {
"message": "Write today's one-line reminder of our refund window."
},
"result": {
"target_type": "agent",
"result_id": "gen_TcQ6rGwVeOXdXfqf",
"status": "completed",
"output": "Don't forget: You have 30 days from delivery to request a refund."
},
"error": null
}

result.result_id is the generation the firing produced. A failed firing carries error instead — usually the agent's provider refusing the call.

export GENERATION=gen_TcQ6rGwVeOXdXfqf

3. Read the generation it produced​

The generation is an ordinary one, with one difference: it names the trigger that started it.

naturali get-generation \
--project-id "$PROJECT" \
--generation-id "$GENERATION"
{
"id": "gen_TcQ6rGwVeOXdXfqf",
"agent_id": "agent_7VDoEY4pPnWYUyMV",
"status": "completed",
"stop_reason": "stop",
"trigger_id": "trg_KipkvLcSYfHYNBmA",
"agent_version": 1,
"usage": { "cost_usd": 0.00000911, "input_tokens": 33, "output_tokens": 17 }
}

trigger_id is what makes an unattended run traceable: any generation can be followed back to the schedule that started it.

4. Read the firing log​

Every firing — yours and the scheduler's — lands in one log per trigger. This is where you check, tomorrow, that the 09:00 run happened.

naturali list-trigger-firings \
--project-id "$PROJECT" \
--trigger-id "$TRIGGER"
{
"data": [
{
"id": "trg_fire_WCXDKoYaTTSPY2vm",
"trigger_id": "trg_KipkvLcSYfHYNBmA",
"source": "manual",
"status": "succeeded",
"result": {
"target_type": "agent",
"result_id": "gen_TcQ6rGwVeOXdXfqf",
"status": "completed"
}
}
],
"total": 1,
"limit": 50,
"offset": 0
}

After 09:00 UTC the next entry reads "source": "schedule" — the schedule running without you.

Pausing the schedule​

active: false stops the firings and keeps the trigger; true resumes it. A negative credit balance also sets it to false, and nothing turns it back on for you — see A debt turns a schedule off.

naturali update-trigger \
--project-id "$PROJECT" \
--trigger-id "$TRIGGER" \
--active false

What's next​