Skip to content

Run on a schedule

View as Markdown

This guide runs an agent or a flow at set times, and shows each time it ran, or why it didn't.

A schedule starts a run of an agent or a flow at the times a cron expression gives. Its runs act as whoever made the schedule, checked again at every run. They land in the schedule's project, and each names the schedule that started it.

To start a run when something happens outside instead, such as an order in your WooCommerce store, use a webhook trigger. For each new mail in a mailbox, use a mail trigger. Or have the other system start a run with POST /v1/runs.

  • An agent or a flow to run, registered on your runtime, such as kindgi dev. This guide runs the agent acme.order-desk.
  • Kindgi 0.2's CLI, in your pack's directory.
  • write on the schedule's project and execute on the agent or flow, when authorization is on: its runs act as you, unless you make it shared (Run as a service account).

The schedule says what to run, with what input, and when:

Terminal window
pnpm exec kindgi schedules create --agent=acme.order-desk --cron="0 7 * * 1-5" --timezone=America/Toronto \
--input='{"userMessage":"List the orders still open from yesterday."}' --label="weekday digest"
  • What it runs: --agent runs the agent's live version, or else its latest. --agent-version pins one. --flow runs a flow, and needs --flow-version.
  • Its input: an agent's is {"userMessage": "…"}, the message each run sends. A schedule for an agent without one is refused when you create it. A flow takes its own input, {} if you give none. --input=@digest.json reads a file.
  • When: --cron takes a cron expression or a shorthand such as @daily. 0 7 * * 1-5 is 07:00 on weekdays. The times are in --timezone, an IANA name, UTC by default. The shortest interval is a minute, except under kindgi dev.
  • Where: --project=<project-id> is its project. Without it, it's the tenant's Default project.
  • --label names it, for lists and the console.
  • --allow-unattended=<tool-id> names a tool whose calls in its runs needn't wait for a person's yes, though no person started them, once for each tool. It takes admin on the project (Tools allowed unattended).

It prints the schedule, with its id. To see the next three times it runs, where <schedule-id> is that id:

Terminal window
pnpm exec kindgi schedules get <schedule-id> --upcoming=3

From your app, the clients have the same calls on schedules: register, list, get, update, pause, resume, runNow, fires, takeOwnership and unregister (run_now and take_ownership in Python).

A schedule's runs act as its owner. A schedule you create is personal: it runs as you, with your connections, and only you or a project admin can change it (Shared or personal). To make it shared instead, so its runs act as a service account, with the account's grants rather than yours, and keep running after you leave, run it as the project:

Terminal window
pnpm exec kindgi schedules create --agent=acme.order-desk --cron="0 7 * * 1-5" --timezone=America/Toronto \
--input='{"userMessage":"List the orders still open from yesterday."}' --project=<project-id> --run-as=project
  • --run-as=project takes admin on the project (runtime 0.2.0 or later), as well as what creating the schedule needs. Anyone else with write on the project is answered 403 permission-denied, "Running a trigger as the project takes a project admin." Its runs act as the project's own service account, made when a trigger is first set to run as the project, with the editor role on the project, so they may do what the project's editors may (A project's own account). With an authorization store, an agent or flow the project's editors can't run is refused, 409 trigger-owner-cannot-run-target: "The project's own account runs only what the project's editors can: run the trigger as a service account that can, or own it yourself."

For an account of your own instead, one used across projects, with other grants, or with keys for an app, create a service account, give it the schedule's project, and name it with --run-as:

Terminal window
pnpm exec kindgi service-accounts create acme-scheduler --description="Runs the morning schedules" --project=<project-id>:editor

--project=<project-id>:editor gives the account a role on the schedule's project that runs agents and flows there. create prints its serviceAccountId, the <service-account-id> below. To give an account you already have the project, use kindgi service-accounts grant <service-account-id> --project=<project-id> --role=editor (Service accounts).

Terminal window
pnpm exec kindgi schedules create --agent=acme.order-desk --cron="0 7 * * 1-5" --timezone=America/Toronto \
--input='{"userMessage":"List the orders still open from yesterday."}' --project=<project-id> --run-as=<service-account-id>
  • Naming an account needs act_as on it, which tenant admins hold, as well as what creating the schedule needs. Anyone else is answered 403 permission-denied, "You can't name that service account as the trigger's owner.", whether or not the account exists. A tenant admin is told when no service account in the tenant has that id (404 service-account-not-found), it was unregistered (409 service-account-unregistered), or it's a project's own account (409 service-account-project-owned, "That's a project's own account: run a trigger as it with runAsProject.").
  • The account must be able to run it, as any owner must. With an authorization store, that's checked when you name it: an account that can't run what the schedule runs is refused (409 trigger-owner-cannot-run-target). Give it the editor role on the target's project, or name another account. Without a store, grants limit nothing, so there's nothing to check.
  • A new target is checked against the owner too: a change keeps the schedule's owner, so with a store, a new agent or flow must be one the owner can run, an account or a person (409 trigger-owner-cannot-run-target). It isn't checked when you're the owner, nor for an owner that was unregistered or removed. A tenant admin can give an account the role (not the project's own account, whose role is fixed), or a project admin can take the schedule over first.
  • A fire it may no longer run is refused: after a grant is taken away, and every fire after the account is unregistered (runtime 0.1.9 or later). Take the schedule over, or delete it.
  • A schedule made with a token the runtime made at startup is personal to that start's user. Without KINDGI_API_TOKEN or KINDGI_SEED_USER_ID, each start makes a new token, with a new user, so the schedule stays with a user later starts no longer use. Make such schedules shared with --run-as=project, or set KINDGI_API_TOKEN or KINDGI_SEED_USER_ID, so the token's user stays the same across restarts (Point the runtime at it).
  • To make an existing schedule shared: kindgi schedules take-ownership <schedule-id> --run-as=project, or --run-as=<service-account-id> for an account of your own. It needs what taking it over yourself needs, admin on the project and execute on the agent or flow, and act_as on an account named by id. Its next fire acts as the account. The schedule keeps its tools allowed unattended.
  • From your app: runAsProject: true or runAsServiceAccountId on schedules.register and schedules.takeOwnership (run_as_project and run_as_service_account_id in Python). Send one, not both: both is a 400.
  • Each change of owner is recorded as evidence, trigger-owner-set: a schedule created to run as an account, and every take-over. It says projectAccount: true when the account is the project's own.

A schedule a person owns is personal: its runs act as them, with their connections. One a service account owns is shared: its runs act as the account, with the company's connections only. Its owner is whoever created it, or the account named with --run-as, and no change moves it (runtime 0.2.0 or later):

  • A personal schedule is its owner's. Only they or a project admin can change it, pause or resume it, run it now or unregister it. Anyone else with write on the project is answered 403 permission-denied, with its owner's name, such as "This schedule is personal: only Sam Lee or a project admin can change it." The owner still needs write on the project, and its tools allowed unattended always take a project admin.
  • What a shared schedule runs takes a project admin: its agent or flow and version, its input, and its tools allowed unattended. Anyone else with write on the project is answered 403 permission-denied, "Changing what a shared schedule runs takes a project admin." So a deploy job's key that moves a shared schedule to a new version needs admin on the project too. Its label, its cron line and timezone, its catch-up, overlap and deadline policies, and pausing, resuming, running it now or unregistering it take write on the project, as before.
  • A project admin's change to a personal schedule keeps its owner, so its runs still act as them, with their connections. Each change, pause, resume, run-now or unregister by anyone but the owner is recorded as evidence, trigger-changed-for-owner. It names who did it and what: the fields changed, and what the schedule ran before and after, never an input's values. A change to its tools allowed unattended keeps its own record, trigger-tools-allowed-unattended-set. The owner isn't told yet: a later runtime emails them. An app built on @kindgi/api can tell them itself, with createApp's triggerOwnerNotices: its tell is called after each such change, and after a take-over from them, with a TriggerOwnerNotice that names the fields changed, never their values.
  • Only a take-over moves the owner. kindgi schedules take-ownership needs admin on the project. Anyone else is answered 403 permission-denied, "Taking a schedule over takes a project admin." A project admin's own take-over makes the schedule personal to them, and one with --run-as (project, or a service account's id) makes it shared (Run as a service account).
  • Without an authorization store, a project admin here is a tenant admin (Without authorization).

Once it's created, the schedule fires at its times. Each time the schedule comes due is a fire. A fire starts one run with the schedule's input, and the run's trigger names the schedule, the fire and the time it was for. With several runtime instances on one database, each time still fires once.

  • The previous run is still going, working or waiting for an approval. With --overlap=skip, the default, the fire is skipped-overlap. --overlap=allow starts another run.
  • The runtime was down when one or more times came due. With --catch-up=latest, the default, it fires once when it's back, for the latest of them. It says how many it stood in for, as missedCount. --catch-up=skip fires none of them. A time is missed when the fire would start more than --starting-deadline seconds late, 600 by default.
  • Its owner may no longer run it, such as when they lost their role on the project, or were removed from the tenant, or it's a service account that was unregistered (runtime 0.1.9 or later). The fire is refused, with why. Whoever holds admin on the project can make themselves its owner: kindgi schedules take-ownership <schedule-id>, or hand it to a service account (Run as a service account).
  • The person a run would act for is being erased: the fire is skipped-erasure, and no run starts for them until the erasure completes.

After 5 fires in a row that were refused or failed, the schedule pauses itself, with the reason in its statusReason. Skipped fires don't count. Nor does a fire refused for a limit, its note starting rate-limited:, or one the database was too busy to start (runtime 0.1.9 or later), its note starting database-busy:. Neither starts the count over. kindgi schedules resume <schedule-id> clears the pause.

The schedule's history says what each fire did:

Terminal window
pnpm exec kindgi schedules fires <schedule-id> --table

--table prints a table. Without it, the same fires come as JSON. The history is newest first, kept for 30 days. Each row has:

  • FIRED: when the fire happened.
  • FOR: the time it was for, or run-now for one you started by hand.
  • OUTCOME: started, skipped-overlap, skipped-erasure, skipped, refused, failed, or pending while a fire is starting. A fire is skipped when an improve schedule's threshold isn't met or its monthly cap is reached, when its project is suspended, or when its tenant is suspended or starts closing before its run or Improve run has started. NOTE says which.
  • STARTED: a run's id, or pass <id> for an improve schedule.
  • NOTE: why it was skipped, refused or failed, or how many missed times it stood in for.

Improve on a schedule shows a history. kindgi runs list --trigger=<schedule-id> lists the runs it started. Each fire also logs one [schedules] record, with its outcome and run, as Logs describes.

Terminal window
pnpm exec kindgi schedules list --status=active
pnpm exec kindgi schedules list --project=<project-id>
  • --status=active lists the active ones.
  • --project=<project-id>, ?projectId= in the API, lists one project's schedules.

Each schedule's owner has the owner's name now, displayName: the person's, or the service account's. When it can't be read, only its id is there. The CLI's RUNS AS column shows that name, else the id, and whether the schedule is personal or shared, such as "Sam Lee (personal)". A schedule that runs as its project's own account has isProjectAccount: true in its owner, and its RUNS AS says "this project (shared)".

With authorization on, the list holds the schedules in projects you may read, or all of them for a tenant admin. A page can then hold fewer than --limit and still have more after it. Keep going with --cursor, set to the previous page's nextCursor, until there's none.

  • kindgi schedules run-now <schedule-id> runs it once now, outside its schedule. Its next time doesn't move, and a run still going doesn't stop it.
  • kindgi schedules pause <schedule-id> stops it. resume starts it again from its next time, without running the ones it missed while paused.
  • kindgi schedules update <schedule-id> changes when it runs, what it runs, its input or its policies, with the same flags as create. Its owner stays as it is (Shared or personal). --allow-unattended replaces its tools allowed unattended, and a new --agent or --flow must name them again, or none. A new --improve must name none.
  • kindgi schedules unregister <schedule-id> removes it.

A project's Schedules page lists its schedules. Each row says:

  • its label, its cron in words, such as "Every day at 2:00 AM UTC", and the cron;
  • whether it's Active or Paused;
  • what it runs, such as "the agent Dev-Echo Agent (its pinned version, or the latest)", "the flow Refunds 1.0.0" or, for a schedule that starts Improve runs, "Improve runs on Acme Rank Scorer, when enough new answers have been marked wrong";
  • when it next fires, such as "Next in 23h (25 Oct 2026, 10:00 PM EDT)", or that it's paused;
  • its last scheduled fire, and Runs it started →. A Run now fire is on the schedule's own page, not here.

Each row has Pause or Resume, Run now and Remove. Remove can't be undone: the runs it started stay, and you create it again to start over. Times are on your own clock to start with: Times, in the menu under your name, has Local with your city, such as Local (Toronto), and UTC (The menu under your name). A cron's words keep the schedule's own zone. The list has a search box and sorts by a column's header. Columns chooses which columns it shows. Schedule Improve, at the top, makes a schedule that starts Improve runs (Schedule Improve in the console).

The Schedules page, with Schedule Improve at the top, a search box and the Columns menu: the paused weekly report (Mondays at 9 AM UTC, 0 9 * * 1; the flow Refunds 1.0.0) with Resume, and the active nightly digest (every day at 2 AM UTC, 0 2 * * *; the agent Dev-Echo Agent, its pinned version, or the latest; when it next fires) with Pause, neither fired yet, each with Runs it started →, Run now and Remove.

A schedule's page has:

  • When: its cron in words and the cron, in the schedule's own zone, and its next five times, each in that zone (named when it isn't UTC) and also on your clock when that reads differently. A paused one says "It doesn’t fire while paused." instead.
  • Runs as: whose access its runs have, checked again at every fire, such as "Runs as you, with your access."
  • What it fired: a summary, such as "Of its 2 fires, 2 started a run.", and a table of its fires, newest first. Each fire has when it fired, the time it was for, what happened or why the fire was refused, and the run it started. A fire started by hand is for "run now", and what happened reads "Started a run (run now)". The table sorts by a column's header, and Columns chooses which columns it shows.
  • Details, folded at the foot of the page: its trigger and schedule ids, its owner, and Tools allowed unattended, the tools its runs call without waiting for a person's yes, or "None" (Tools allowed unattended).

The nightly digest's page: Active, with Pause, Run now, Change owner… and Remove, "Starts the agent Dev-Echo Agent (its pinned version, or the latest). Next in 15h (27 Oct 2026, 10 PM EDT).", When: "Every day at 2 AM UTC", 0 2 * * * and its next five times in UTC, each also on the viewer's clock in EDT, Runs as: "Runs as you, with your access." and "Checked again at every fire: if its owner can no longer run what it runs, the fire is refused, and after repeated refusals the schedule pauses itself.", What it fired: "Of its 2 fires, 2 started a run.", with Runs it started →, the Columns menu and two fires, each for run now, "Started a run (run now)" and its run, and Details, folded.

Project editors and up have, on the list and on the page:

  • Pause and Resume;
  • Run now: "It starts a run now, as its owner, with its usual input. Its next scheduled fire doesn't change.";
  • Remove: "Remove this schedule? It stops firing. The runs it started stay, with their record of what started them. It can't be brought back: create it again to start over."

On a schedule that runs as someone else, project admins also get Take over: "Its next runs start as you, with your access. Runs already started keep theirs. If it's paused, it stays paused." On a schedule with tools allowed unattended, the dialog adds that its runs still call them, with the new owner's access.

The Take this schedule over? dialog on a paused schedule that runs as a service account: its next runs start as you, runs already started keep theirs, and a paused one stays paused.

After it, the page says "Runs as you, with your access.", and a paused schedule stays paused:

The weekly report's page after Take over: Paused, with Resume, Run now, Change owner… and Remove, "Starts the flow Refunds 1.0.0. Paused.", When: "Mondays at 9 AM UTC", 0 9 * * 1 and "It doesn’t fire while paused.", Runs as: "Runs as you, with your access." and "Checked again at every fire: if its owner can no longer run what it runs, the fire is refused, and after repeated refusals the schedule pauses itself.", What it fired: "It hasn’t fired yet. Runs it started →", and Details, folded.

A tenant admin gets Change owner… instead (runtime 0.1.9 or later), whenever the tenant has a registered service account the schedule doesn't already run as. Its Runs as list has "You — with your access", the take-over above, and each registered service account, such as "nightly-bot — a service account, with its own access". The account it runs as now isn't listed, and on a schedule that runs as you the list starts at "Pick a service account". Under the list, the dialog says what changes before anything does, such as "Its next runs act as nightly-bot, with its access, not yours: it needs its own access to what this schedule starts." After Change owner, the page reads: "Runs as the service account nightly-bot." Naming an account needs what --run-as needs, and a refusal shows in the runtime's words.

A run a schedule started says so on its page, under Started by: "The schedule “nightly digest”", when it fires, such as "Every day at 2:00 AM UTC", and for a scheduled fire the time it was for. Then whom the run acts for, such as "Acts for you", and Its other runs →, the runs list filtered to that schedule.

A run's page whose Started by says "The schedule “nightly digest”", "Every day at 2 AM UTC", "Acts for you" and Its other runs →.

A schedule can start Improve runs instead of agent or flow runs, when enough new corrections have come in on an agent's runs, counted the way the Improve run will count them:

Terminal window
pnpm exec kindgi schedules create --cron="0 3 * * *" --improve=acme.rank.scorer --project=<project-id> --segment=company:acme \
--input='{"tiers": ["settings"], "threshold": {"judgments": 5, "runs": 3, "judges": 2}, "monthlyCapUsd": 20}' --label="acme improve"
  • --improve=acme.rank.scorer is the agent to improve.
  • --segment=company:acme narrows its scope to a segment of the project.

It prints the schedule, with the defaults applied to its input:

{
"scheduleId": "fa6356f8-3f9f-4ab3-b713-636424a43120",
"triggerId": "fa6356f8-3f9f-4ab3-b713-636424a43120",
"improve": {
"agentId": "acme.rank.scorer",
"scope": {
"kind": "segment",
"projectId": "eea49512-700f-4c59-9f66-05ca3b820a3b",
"path": [
{
"key": "company",
"value": "acme"
}
]
}
},
…
"input": {
"tiers": [
"settings"
],
"budget": {
"maxCostUsd": 5,
"maxCandidates": 30
},
"objective": "weightedYesShare",
"threshold": {
"runs": 3,
"judges": 2,
"judgments": 5
},
"classWeights": "restricted-only",
"monthlyCapUsd": 20
},
…
}
Excerpt

At each fire, it counts the corrections that marked the agent's runs wrong ("no") in its scope since the agent's last Improve run there, the way the Improve run will count them: everyone's until the tenant, the project, or the agent being improved has a restricted judge class, then only the ones recorded under one. With classWeights in the schedule's input, it counts the way that value says. The last Improve run is one that didn't fail or get cancelled, whoever started it. With enough of them, across enough runs, from enough judges, it starts an Improve run on fresh test cases of those runs, improve.<pass-id>. Otherwise the fire is skipped, and its note says which count was short.

That schedule's first fire, started with run-now, was skipped: one judge short. Then the threshold was lowered to one judge:

Terminal window
pnpm exec kindgi schedules update <schedule-id> --input='{"tiers": ["settings"], "threshold": {"judgments": 5, "runs": 3, "judges": 1}, "monthlyCapUsd": 20}'

It prints the schedule, with its new input. The next fire started an Improve run, and the third came while that Improve run was still running. Its history, newest first:

Terminal window
pnpm exec kindgi schedules fires <schedule-id> --table
FIRED FOR OUTCOME STARTED NOTE
──────────────────────── ─────── ─────────────── ───────────────────────────────────────── ──────────────────────────────────────────────────────────────────────────────────────────────────────────────
2026-10-09T13:25:40.917Z run-now skipped-overlap Its previous Improve run 3599f4ab-8788-433f-af66-7990ddfa9f47 is still running.
2026-10-09T13:25:40.341Z run-now started pass 3599f4ab-8788-433f-af66-7990ddfa9f47
2026-10-09T13:25:39.201Z run-now skipped Not enough new experts' corrections so far: 1 of 2 experts (enough: 10 answers experts marked wrong, 10 runs).

This project has a restricted judge class, so the Improve run counts experts' corrections, and the skip note says "answers experts marked wrong" and "experts". Without one, it would count every answer marked wrong, and the note would say "answers marked wrong" and "people who corrected".

  • Its scope is the schedule's project, --project, or a segment of it, --segment=<key:value>, repeated for a path. It's never the tenant or an org: an Improve run learns from one project's runs, so its evidence must cover the scope it changes. Make it live more widely by hand after review.
  • Its input, --input, takes the Improve run's options, threshold and monthlyCapUsd. It's kept with the defaults applied: the thresholds default to 5 corrections (judgments), across 3 runs (runs), from 2 people (judges), and the cap to 20 US dollars a month.
  • A monthly cap: once the schedule's Improve runs this month, in UTC, have cost monthlyCapUsd, its fires are skipped until the next month. An Improve run's budget is never more than what's left of it. Its cost counts the runs on the test cases that prove its best candidate, which its budget doesn't stop, so a month's Improve runs can cost more than the cap.
  • One Improve run at a time: while the schedule's previous Improve run is still going, a fire is skipped-overlap.
  • At most once an hour. Registering needs publish on the agent.

A fire that started an Improve run names it, as passId. The Improve run names the schedule and the fire, as trigger. When the Improve run suggests a change, it asks for the review at once, so the suggested change waits in reviewers' approvals.

Schedule Improve makes the same schedule in the console: on the project's Schedules page, where it asks which agent, or on an agent's Suggested changes page, for that agent. It shows to someone who can change the project's schedules (write on the project) and start Improve runs (publish on the agent). It asks for what --input and the flags take, with the same defaults:

  • What to improve: Settings values, or The agent's instructions with a Drafting model and how many Drafts of the instructions;
  • Better means a higher: share marked right, or weighted precision@k;
  • Learn from experts' corrections only (recommended), ticked to start with, for classWeights restricted-only; unticked, it's as-recorded;
  • its scope: the project, or a segment of it;
  • When (cron) and Time zone, an IANA name, UTC to start with. Under them it puts the cron in words, and it holds back one that fires more than once an hour: "At most once an hour: name one minute (0 * * * * is every hour). Each fire may start an Improve run, which costs money."
  • Starts an Improve run at: Corrections marking an answer wrong, On runs and From judges, 5, 3 and 2 to start with. "Runs and judges can't be more than corrections: each correction is on one run, by one judge."
  • Each Improve run spends at most (US$), 5, and for settings values And tries at most, 30; Its Improve runs spend at most, a month (US$), 20: "A calendar month, UTC. An Improve run never spends more than what's left of it.";
  • Label (optional).

Create makes it, running as you, and opens its page. Under kindgi dev, Schedule Improve is off, and the page says why: a schedule's Improve runs are each for one project, and a change under kindgi dev is for every project. Start Improve from the agent's Suggested changes page instead.