Scheduled Apps
Author an app, run it once by hand, then put it on a timetable with a cron trigger that fires it unattended in your own timezone.
A scheduled app is a piece of work you have already taught the agentic runtime to do, set to happen on its own at a time you choose. You write it once, watch it run once, and then hand it a timetable. After that it runs without you.
This page walks the whole lane end to end: author, run, schedule.
What you need first
A running gateway. Start one on your own machine with
kx serve --dev-allow-local. Everything below talks to it.
See Install if you do not have kx yet.
The steps in the example below are model steps, so the gateway also needs a model served to it — see Serving. The schedule itself lives inside the gateway, so the gateway process has to stay running for a scheduled app to fire.
The three pieces
An app is a saved, reusable unit of work — a kortecx.app/v1 envelope holding a
blueprint (the steps and how they connect), by-reference pointers to the context,
tools and connections it may use, and its steering settings. It carries no authority
of its own: every time it runs, the runtime re-resolves what it is allowed to do from
your grants.
A trigger is an inbound event bound to an app. There are three kinds — webhook,
cron and grpc. A cron trigger is the one that carries a timetable.
The cron ticker is a task inside the gateway that wakes up every few seconds, looks for triggers whose scheduled moment has arrived, and fires them.
Author the blueprint
A blueprint is the shape of the work: the steps, and which step feeds which. The
quickest way to write one is the chain DSL, where > means "then" and & means
"at the same time".
Write a tasks.json describing each step:
{
"draft": {
"kind": "model",
"prompt": "Write a short standup summary from yesterday's notes."
},
"review": {
"kind": "model",
"prompt": "Tighten the summary. Remove anything that is not a fact."
}
}Then lower it to a portable blueprint file without submitting anything:
kx chain run "draft > review" --tasks tasks.json --dry-run --emit-blueprint standup.blueprint.json--dry-run compiles and validates the graph and needs no gateway. If a handle is
unknown, or the graph has a cycle, it fails here rather than at run time.
Steps inside one app can fan out and fan back in — "plan > [a & b] > gather" runs
a and b in parallel and merges them. If you would rather not write that topology
yourself, kx swarm --pattern supervisor builds it and runs it directly (it runs a
chain; it does not write a blueprint file for you). Chaining always happens inside
one app; an app does not call another app.
Wrap it as an app and save it
kx app new wraps the blueprint in an envelope. It is offline — no gateway contact —
unless you attach catalog skills with --skill.
kx app new standup --from-blueprint standup.blueprint.json --description "Daily standup summary" --output standup.app.jsonkx app save standup.app.jsonsave validates and canonicalizes the envelope and stores it in your own catalog.
The handle defaults to apps/local/standup — derived from the name, lowercased, with
anything unusual turned into a hyphen. Pass --handle to choose your own.
Check it landed:
kx app listFill in the project files
An app can also have a small project tree — the written material behind it: what it
is for, how it should behave, what it must refuse. kx app scaffold has the runtime
author that tree for you, file by file, into the app's own copy-on-write branch. It
needs a served model (Serving); without one the command reports that
this gateway has no scaffold orchestrator.
kx app scaffold apps/local/standup --goal "summarise yesterday's notes into a standup update" --waitFor a scheduled app the structure is fixed. These five files are the skeleton:
README.md says what the app does. app.json describes it as data. prompts/system.md
is a system prompt for the app. rules/guardrails.md is the scope, safety and refusal
rules. skills/main.md is the primary instruction plus the tools it may use.
List and read them:
kx app files apps/local/standupkx app cat apps/local/standup rules/guardrails.mdThe project tree is source, not wiring
These files live in the app's branch. A run folds in the prompts, rules, context and
memory the envelope points at by reference — the envelope's branch handle is
reserved and is not read at run time. So scaffolding a rules/guardrails.md does not
by itself change what a run does; you still have to reference the text you want from
the app itself. See Editing an app.
Run it once, by hand
Never schedule something you have not watched run.
kx app run apps/local/standup --wait--wait blocks until the run reaches a terminal state and prints the result. Add
--out result.txt to write the body to a file, and --arg key=value (repeatable) to
pass inputs, which are folded into the app's entry model step as an "Inputs" block.
If the app can do something irreversible, run it the careful way first:
kx app run apps/local/standup --require-approval --waitWith that flag, a world-mutating tool call pauses instead of firing. You release it yourself:
kx approvals listkx approvals grant <REQUEST_ID>Read more in Approvals.
Put it on a timetable
Now bind a cron trigger to the app. This is the end state:
kx triggers add --name standup --kind cron --app apps/local/standup --cron "0 9 * * 1-5" --timezone America/New_York --require-approvalfrom kortecx import KxClient
with KxClient("http://127.0.0.1:50151") as kx:
trigger_id = kx.triggers.add(
"standup",
kind="cron",
app="apps/local/standup",
schedule="0 9 * * 1-5",
timezone="America/New_York",
require_approval=True,
enabled=True,
)
print(trigger_id)import { KxClient } from "@kortecx/sdk";
const kx = new KxClient("http://127.0.0.1:50151");
const { triggerId } = await kx.triggers.add({
name: "standup",
kind: "cron",
appHandle: "apps/local/standup",
scheduleSpec: "0 9 * * 1-5",
timezone: "America/New_York",
requireApproval: true,
enabled: true,
});
console.log(triggerId);
kx.close();Read the schedule left to right: minute, hour, day-of-month, month, day-of-week. So
0 9 * * 1-5 is "at 09:00, Monday through Friday" — and --timezone America/New_York
means 09:00 there, not in UTC. The timezone is an IANA name; leave it off and the
schedule is evaluated in UTC.
On the CLI, a trigger is off until you enable it
kx triggers add registers the binding disabled unless you pass --enabled. A
disabled trigger never fires, and kx triggers fire refuses it. Add the flag once you
are ready for it to start firing, or register it first, dry-run it with
kx triggers test (which works while it is disabled), then run kx triggers add
again with the same --name plus --enabled — a re-add with the same name replaces
the binding. The Python and TypeScript SDKs take the opposite default and register it
enabled unless you say otherwise.
Exactly one of --app <handle> or --recipe <handle> is required. Use --app here:
the app fires unattended with its declared connections and secret scope resolved.
--require-approval puts the same human gate on every scheduled run — a scheduled
fire goes through the same authoring path as kx app run --require-approval. An
unattended run that wants to do something irreversible will wait in
kx approvals list for you rather than proceeding on its own.
Writing the schedule
The schedule field accepts two shapes, and the runtime tells them apart by whether it is all digits.
| What you pass | What it means |
|---|---|
--cron "0 9 * * 1-5" | A standard 5-field crontab expression, evaluated in --timezone |
--cron 300 | A plain interval: fire every 300 seconds |
--schedule is an alias for --cron, and --tz is an alias for --timezone. Both
name the same field.
Some things worth knowing before you write one:
- Exactly five fields. A 6-field expression with a seconds column is rejected.
- Daylight saving is handled. The next fire time is computed in the named zone with DST-correct arithmetic, so a 09:00 job stays at 09:00 across the change.
- Strictly forward. The next fire is always computed as strictly after now, so a job that is due at this exact instant schedules for its next occurrence, never the one happening now.
- An interval must be above zero.
--cron 0is rejected. - A typo fails immediately. An unparseable expression, or a timezone that is not a real IANA name, is rejected when you register the trigger — not silently turned into a trigger that never fires.
A duplicate tick fires once
The gateway does not just call your app on a timer and hope. Each cron trigger keeps a watermark: the timestamp of the moment it is next due.
On each tick the ticker advances that watermark first, before it fires anything, so neither a slow start nor the following tick can pick the same moment up twice. It then fires with an idempotency key derived from the trigger's name and that exact watermark. If the same scheduled moment somehow reaches the runtime twice — a retry, a race — the second one is recognised as the same event and returns the run the first one already started. One scheduled moment, one run.
What this does not promise
The ticker scans for due triggers every few seconds, so a run starts close to its scheduled minute rather than exactly on it. And if a fire itself fails, that is one missed run — the next scheduled time fires normally. The local scheduler makes no exactly-once delivery claim; what it guarantees is that a duplicate or raced tick does not double-fire. The gateway also has to be running at the scheduled moment: a gateway that is stopped at 09:00 does not fire that occurrence.
The watermark is stored outside the journal, so restarting the gateway re-reads it and picks the schedule back up where it was.
Checking and changing a trigger
Dry-run the binding without firing anything — this checks the handle resolves and the payload binds:
kx triggers test --name standupSee everything registered, with its kind, target and whether an auth secret is attached:
kx triggers listFire one on demand, out of schedule (the trigger has to be enabled):
kx triggers fire --name standup --idempotency-key manual-2026-07-20The idempotency key is yours to choose. Replaying the same key returns the run the first one started rather than making a second one. Leave it off and the runtime derives one from the payload.
Remove it:
kx triggers rm --name standupWhen a scheduled run goes wrong
Nobody is watching at 09:00. So the runtime keeps the record for you.
- Run history.
kx runs listshows durable runs newest-first, scheduled ones included. - Failures. Terminal failures land in an inbox instead of vanishing —
kx alerts list, orkx alerts list --instance <INSTANCE_ID>for one run (the run's 16-byte instance id, written as hex). - Spend.
kx cost <INSTANCE_ID>gives a local spend estimate at rates you configure, with a ceiling — not a bill. - Metrics. There is an opt-in Prometheus
/metricsendpoint if you want to graph it. See Observability.
Notes and limits
- A trigger runs under your party. The trigger id is derived by the server, and the run binds to the identity of whoever registered it — never to something the client claims.
- Secrets are referenced by name only.
--secret-refnames a secret in the local store; the value never travels over the wire. - Scheduling is single-system by default. One gateway owns its triggers; running a schedule across multiple nodes is roadmap, not today.
- A cron trigger fires the app with an empty payload. If your app needs inputs, put them in the app itself rather than expecting them from the tick.
- Only a functional app can be scheduled. A hosted app is refused at registration.
Apps
An App is the named, saved unit of work in the agentic runtime — a kortecx.app/v1 envelope that carries references and intent, never authority.
Hosted Web Apps
Describe a small web tool and the agentic runtime builds it, installs it, and runs it on your own machine so you can open it in a browser.