Automations

Work that starts without anyone asking.

A trigger starts a session with no person present. A cron schedule fires it on the clock; a signed webhook fires it on an event. Either way the agent gets its own cloud computer, its own branch, and the same review on the way back.

Two types · Declared in kortix.yaml · Reviewed like everything else

Types
cron and webhook
Schedule
6-field cron, any IANA timezone
Webhook auth
HMAC-SHA256, no unsigned path
Runs as
An agent you name
Two types

Two types. There is no third.

A trigger is a clock or a signature. Everything else about it — which agent it runs as, what it says, which session it lands in — is the same config either way.

cron

It fires on the clock

A 6-field cron expression — second, minute, hour, day, month, weekday — in any IANA timezone. Or a single run_at timestamp, for something that should happen once and then stay quiet.

webhook

It fires on an event

An external service POSTs to the trigger URL. Kortix checks the signature, renders the payload into the prompt, and starts the session. A payload that fails your filter is accepted and ignored.

  • Both types name the agent they run as, and inherit that agent’s deny-by-default reach.
  • Both carry a prompt template that becomes the session’s first message.
  • Both are entries in kortix.yaml, so both have a history and an author.
The cron surface

A schedule you can read in one column.

Every trigger in a project is one row: what it is called, when it fires, in whose timezone, as which agent, and which session that fire lands in. Nothing about it is hidden state you have to click into.

TriggerCronTimezoneAgentSession
daily-digest0 0 9 * * 1-5Weekdays at 09:00America/Los_Angeleskortixfresh
invoice-sweep0 30 6 1 * *The 1st of the month at 06:30Europe/Berlinfinancereuse
oncall-handoff0 0 17 * * 5Fridays at 17:00UTCsupportfresh
roadmap-review0 0 8 * * 1Mondays at 08:00America/New_Yorkplannerpinned
Timezones are real
A trigger stores an IANA timezone name, not an offset, and defaults to UTC. Set America/Los_Angeles and it stays at 09:00 local across a daylight-saving change. An abbreviation like PST is rejected rather than guessed at.
You do not have to write cron
The Schedules screen is a picker — every few minutes, weekdays, every month, or once at a moment you choose. Raw cron is the escape hatch behind it, not the price of entry.
One switch pauses everything
A project-level pause stops every trigger at once, on top of each trigger’s own enabled flag. Use it when the same repo runs on two control planes so nothing fires twice.
A burst queues, it does not drop
A project runs 3 triggered sessions provisioning at once by default. A fire past that limit comes back queued and runs when a slot frees, rather than failing.
Declared in the repo

An automation is a file, not a dashboard setting.

Triggers live in kortix.yaml next to your agents and sandbox images. Each one names its agent, its schedule or its secret, and the prompt template that becomes the session’s first message.

kortix.yaml
# fires on the clocktriggers:  - slug: daily-digest    type: cron    agent: kortix    cron: "0 0 9 * * 1-5"    timezone: America/Los_Angeles    session_mode: fresh    prompt: |      Summarize yesterday’s commits.      Open a change request against main. # fires on an event  - slug: new-lead    type: webhook    agent: sales    secret_env: WEBHOOK_SECRET    prompt: >-      A new lead arrived: {{ body.name }}      ({{ body.email }}). Add it to the CRM.
kortix triggers
# add it, ship it, and the schedule is live$ kortix triggers add daily-digest --type cron \    --cron "0 0 9 * * 1-5" \    --timezone America/Los_Angeles \    --prompt "Summarize yesterday. Open a CR."$ kortix ship→ kortix.yaml pushed. daily-digest is scheduled. # see every trigger and when it last fired$ kortix triggers ls # do not wait for 09:00 to find out$ kortix triggers fire daily-digest→ session started

The prompt is a template

A prompt renders {{ token.dotted.path }} against the payload that fired it. A webhook fire gets {{ body.* }} and the request headers; a cron fire gets {{ cron.schedule }}, {{ cron.timezone }} and {{ cron.scheduled_for }}. A value that is not there renders as nothing — no error, no leftover braces in the message your agent reads.

Webhooks

Signed, or it does not fire.

Every webhook trigger names a project secret that signs it. A trigger without one is rejected at validation — there is no unauthenticated webhook to forget to lock down later.

POST /v1/webhooks/projects/{projectId}/{slug}

X-Kortix-Signature: sha256=<hmac>

HMAC-SHA256 over the raw request body, compared in constant time. The GitHub-compatible X-Hub-Signature-256 header works too, so a repo webhook needs no adapter.

202
Signature valid. The session fired, queued behind the concurrency limit, or deduped against a delivery Kortix already saw.
200
Valid, and deliberately skipped — the project is paused, or the payload did not match the trigger’s filter.
401
Signature and token both missing or wrong. Nothing runs.
404
No such trigger, or it is disabled, or it is not a webhook trigger.
409
The secret named by secret_env has no value set. Fails loudly rather than firing unprotected.

A filter is a dotted path matched against the same payload the prompt sees. It exists to break loops: a source that reports both sides of a conversation would otherwise fire the agent on its own reply.

Session strategy

Which session a fire lands in.

By default every fire is a clean slate. When the work is a running thread rather than a fresh errand, a trigger can re-prompt a session it already owns. Kortix tries the modes in order and falls through on failure, so a fire never simply disappears.

  1. 01

    pinned

    Re-prompt one exact session, named by id. If that session is gone or failed, fall through.

  2. 02

    keyed

    Render a key from the payload, then re-prompt the most recent healthy session stamped with that exact key. One customer, one thread. It never falls through into another key’s session.

  3. 03

    reuse

    Re-prompt the most recent healthy session this trigger created. A pinned trigger falls back here before falling further.

  4. 04

    fresh

    Cut a new branch and boot a new cloud computer. This is the default, and the last resort for every other mode.

A triggered session is visible to the whole project, not private to whoever configured the trigger. It stops itself after 5 minutes idle, so an automation that runs at 3am is not a machine billing until morning.

Overnight

It fires at 3am. A person still decides.

An automation gets no privileges a person would not get. The same isolation, the same scoped reach, the same one road back to main.

It runs as an agent
A trigger names an agent, and inherits exactly that agent’s grants — the connectors, secrets and skills its block in kortix.yaml lists, and nothing else. An agent with no grants gets no access.
It gets its own computer
A fresh fire boots its own isolated machine on its own branch, the same as a session you start by hand. Nothing it installs or breaks touches another session.
Nothing merges itself
Work reaches main only through a change request a person reviews and approves. You read the diff over coffee. The machine never had the last word.
The automation itself has a history
A trigger is a block of YAML in the repo. Who added the 3am job, when, and what it was told to say are all in the log — the same as any other change.
Automate it

Write the schedule. Read the change request.

Open source and self-hostable. Any model, your keys. Kortix Cloud, your own VPC, or fully on-prem.

Kortix Automations | Kortix