Skip to content
KortixKortix
Esc
navigateopen⌘Jpreview
On this page

Triggers

A trigger starts a session on a schedule, from a webhook, or from a monitor.

A trigger starts a session with no person present. Use a trigger to automate recurring or event-driven work.

Trigger types

Kortix supports three trigger types.

  • cron — runs on a schedule you set.
  • webhook — runs when an external service sends a signed request to the project’s webhook URL.
  • monitor — runs a command from your repository 24/7. Each line the command prints to stdout fires the trigger. Experimental: see Monitors.

You define triggers in the project manifest, kortix.yaml. Each trigger holds a prompt that renders as the fired session’s first message. Runtime state — such as the last fire time and status — lives outside the manifest, in the database. Firing a trigger does not create a commit.

Creating, updating, or deleting a trigger through the API, SDK, or dashboard writes directly to the default branch. It does not go through a change request (CR). Editing kortix.yaml inside a session and running kortix ship follows the normal branch and CR flow instead.

Set up a cron trigger

Add the trigger

kortix triggers add daily-digest --type cron \
  --cron "0 0 9 * * 1-5" --timezone America/Los_Angeles \
  --prompt "Summarize yesterday's activity and save it as a daily note."

cron is a 6-field expression: second, minute, hour, day, month, weekday. This command edits your local kortix.yaml only.

Ship it

kortix ship

kortix ship commits kortix.yaml and pushes it. The schedule goes live once this lands on your project’s default branch.

Confirm it runs

kortix triggers ls

The list shows each trigger’s slug, state, and when it last fired. To fire it now instead of waiting for the schedule, run kortix triggers fire daily-digest.

Set up a webhook trigger

A webhook trigger needs a secret. Kortix uses it to check the signature on every incoming request.

Add the secret

kortix secrets set WEBHOOK_SECRET=<a-random-value>

See Secrets for more on secrets.

Add the trigger

kortix triggers add new-lead --type webhook \
  --secret-env WEBHOOK_SECRET \
  --prompt "A new lead arrived: {{ body.name }} ({{ body.email }}). Add it to the CRM."

--secret-env names the secret that signs requests to this trigger.

Ship it

kortix ship

Send it a request

Kortix builds the webhook URL from your project id and the trigger’s slug:

POST /v1/webhooks/projects/<project-id>/<slug>

Send a signed POST request to this URL from the external service. Kortix checks the signature against WEBHOOK_SECRET, then starts a session with the request body available in the prompt. See Webhook signature below for the exact header and format.

By default, each fire starts a fresh session on a new branch. A trigger can instead reuse or pin a session, and a webhook trigger can filter which payloads start one — see Session strategy and Payload templating below.

Monitors

Experimental. Monitors run only where the monitors feature flag is on. The flag is off by default. While it is off, the platform provisions no monitor box and fires no monitor event.

A monitor watches something that neither pushes webhooks nor fits a schedule: a live log, a queue depth, a page that changes, a price. Kortix runs your command 24/7 in the project’s monitor box — one persistent microVM per project, with the same isolation boundary and the same project secrets a session sandbox gets. Deterministic code watches; the agent wakes only when the command emits a line.

Four rules define the contract:

  • Stdout lines are events. Nothing else is. Stderr is diagnostics: visible in the monitor’s logs, never fires.
  • Each line fires the trigger exactly once, through the path a webhook already uses: filter → prompt template → session_mode.
  • A monitor cannot fail silently. Process exit, restart-budget exhaustion, and silence longer than expect_event_within each fire a platform-written lifecycle event in the same stream.
  • session_mode defaults to reuse on a monitor, not fresh. A monitor fires repeatedly by design, so fresh would mint one session per event.

Add the monitor

kortix triggers add checkout-errors --type monitor \
  --run "./monitors/checkout-errors.ts" \
  --mode poll --interval 60s --expect-event-within 24h \
  --prompt "Checkout monitor emitted: {{ line }}"

--mode poll re-runs the command every --interval and expects it to exit. --mode stream runs it once and keeps it alive; a stream takes no --interval. Both shapes produce lines, and nothing downstream can tell them apart. cron, run_at, timezone, and secret_env are rejected on a monitor.

Ship it

kortix ship

The platform starts the monitor box once this lands on the default branch.

Confirm it runs

kortix triggers ls
kortix triggers info checkout-errors

ls shows a monitor’s mode and interval where a cron shows its schedule. info shows run, mode, interval, and expect_event_within.

Monitor limits

Every bound below is enforced by the platform.

Bound Value
Monitors per project 10 enabled
Poll interval at least 30s
expect_event_within at least 5m
Event rate per monitor 60/hour sustained, burst 30. Overflow suppresses the monitor for 10 minutes; 3 suppressions in 24 hours disables it.
Line length 8 KiB, truncated with a truncated: true marker
Restart budget 5 restarts / 10 minutes, then a restart_budget_exhausted lifecycle event and 15-minute backoff
Event retention 30 days
Monthly box budget $75 by default. Past it, the box stops and one budget_exceeded lifecycle event fires.

The monitor box needs a provider that supports a persistent sandbox. Where the project’s provider cannot hold one, the monitors flag reports itself unavailable.

Config shape

# kortix.yaml
triggers:
  - slug: daily-digest # required, lowercase + dashes, unique per project
    name: Daily digest # optional, defaults to slug
    type: cron # "cron" | "webhook" | "monitor", required
    agent: kortix # optional, defaults to "default"
    model: anthropic/claude-sonnet-4-5 # optional, resolves at fire time if unset
    enabled: true # optional, default true
    cron: "0 0 9 * * 1-5" # 6-field expression, mutually exclusive with run_at
    timezone: America/Los_Angeles # IANA name, default UTC
    session_mode: reuse # "fresh" | "reuse" | "pinned" | "keyed", default "fresh"
    filter: # optional, webhook payload guard
      "body.data.direction": "inbound"
    prompt: "Summarize {{ body.text }}" # required, template string

A monitor replaces the schedule fields with its command and shape:

# kortix.yaml
triggers:
  - slug: checkout-errors
    type: monitor
    run: ./monitors/checkout-errors.ts # required, repo-relative command
    mode: poll # "poll" | "stream", required
    interval: 60s # required on poll, invalid on stream
    expect_event_within: 24h # optional silence watchdog
    agent: oncall
    session_mode: reuse # the monitor default
    filter: # optional, same guard a webhook uses
      "line.severity": "error"
    prompt: "Checkout monitor emitted: {{ line }}"

Legacy kortix.toml uses the same fields in a different container; see legacy TOML.

Fields

Field Required Default Notes
slug yes [a-z0-9][a-z0-9_-]{0,127}, unique per project.
type yes cron, webhook, or monitor.
prompt yes Template string. Renders as the session’s first message.
name no slug Human label.
agent no default_agent Must name a key in agents:. Omit it to use default_agent; do not write the literal default.
model no resolves at fire time Wire form provider/model, for example anthropic/claude-sonnet-4-5.
enabled no true When false, the scheduler and the webhook receiver skip the entry.
session_mode no fresh, or reuse on a monitor See Session strategy.
session_id required for pinned Exact session to re-prompt.
session_key required for keyed Template string. Setting it alone implies session_mode: keyed.
filter no Dotted path → expected string. A webhook delivery that does not match returns 200 and fires no session.
cron one of cron/run_at, on type: cron 6-field expression: second minute hour day month weekday.
run_at one of cron/run_at, on type: cron ISO-8601 timestamp. Fires once, then stays dormant.
timezone no, cron only UTC IANA name.
secret_env required, webhook only Name of a project secret holding the signing key. The secret must use broker delivery with the connector consumer.
run required, monitor only Repo-relative command whose stdout lines are the events. One line, at most 1024 characters.
mode required, monitor only poll re-runs run every interval; stream runs it once and keeps it alive.
interval required for mode: poll Duration literal (30s, 5m, 24h, 7d), minimum 30s. Invalid on mode: stream.
expect_event_within no, monitor only Duration literal, minimum 5m. Silence longer than this fires a lifecycle event.

A cron trigger needs cron or run_at, never both. A webhook trigger without secret_env is rejected — there is no unauthenticated webhook. A monitor needs run and mode, and rejects cron, run_at, timezone, and secret_env outright — a manifest that claims a schedule the monitor runner never reads is a lie.

Configure the signing secret before you create or update a webhook trigger:

kortix secrets set WEBHOOK_SECRET=-
kortix secrets delivery WEBHOOK_SECRET broker --consumer connector

Pass the value to the first command on standard input. If the secret previously used sandbox delivery, rotate it after the delivery change because an existing sandbox can retain the previous value.

Session strategy

session_mode controls which session a fire re-prompts. Kortix tries the modes below in order and falls through on failure at each step.

  1. pinned — re-prompt the exact session_id. If that session is gone or failed, fall through.
  2. keyed — render session_key against the payload, then look up the most recent non-failed session previously stamped with that exact key. If the key renders empty, or no session matches, fall through to a fresh session. It never falls through to another key’s session.
  3. reuse — re-prompt the most recent non-failed session this trigger previously created. A pinned trigger falls back here too, before falling further.
  4. fresh — create a new sandbox and branch. This is the default, and the final fallback for every mode. The new session becomes the trigger’s session for future reuse and keyed fires.

Session access

Sessions a trigger creates use the private policy by default. The trigger agent’s service account owns them — an agent’s identity is a service_account principal, so it can own a session and hold assignments like any other principal. A project manager can always open them. An account owner and an account admin hold manager-equivalent access on every project, so the same applies to them. The person who configured or manually fired the trigger does not gain access through that action unless they hold one of those roles.

Trigger settings offer three policies:

  • Trigger agent and project managers — no ordinary project member can open the session.
  • Selected teammates — the trigger agent, project managers, and selected project members or account groups.
  • Whole project — every project member.

This policy is a per-resource visibility setting on top of the role model, not a role. It decides who can open one trigger’s sessions. It grants no permission the role verdict denies. See Accounts & access.

The access policy is account-local runtime state. It does not enter the portable kortix.yaml manifest because principal ids belong to one account. Use the dashboard or SDK session_access field to configure it. Updating only this policy creates no Git commit. Saving a policy also updates prior sessions created by that trigger.

A pinned session keeps its own sharing settings because the trigger did not create it. If a pinned session is unavailable and the trigger creates a fallback session, the trigger policy applies to that new session.

Payload templating

prompt and session_key render with the same engine: {{ token.dotted.path }}. A missing value renders as an empty string — no error, no leftover {{ }}. Objects and arrays render as JSON. session_key is trimmed and truncated to 512 characters.

Every fire also gets {{ trigger.slug }}, {{ trigger.type }}, and {{ trigger.kind }} (always git). The rest of the variable set depends on how the trigger fired.

Source Variables
cron {{ cron.schedule }}, {{ cron.timezone }}, {{ cron.scheduled_for }} (the slot the fire is for), {{ cron.claimed_at }} (when the scheduler picked it up), {{ cron.last_scheduled_for }} (the previous slot; empty on the first fire). No top-level fired_at.
webhook {{ fired_at }}, {{ body.* }} (JSON-parsed; falls back to {{ body.raw }} if the body does not parse), {{ headers.content_type }}, {{ headers.user_agent }}, {{ headers.forwarded_for }}.
monitor {{ line.* }} — the stdout line, JSON-parsed; a line that does not parse renders as {{ line.raw }}. Plus {{ monitor.slug }}, {{ monitor.seq }}, {{ monitor.emitted_at }}, and {{ monitor.kind }} (event or lifecycle).
manual (dashboard “fire now” or the fire endpoint) {{ fired_at }}, {{ source }} (manual), {{ actor }}, {{ message.text }}, {{ message.source }}.

Kortix prefixes every rendered monitor prompt with [MONITOR EVENT — automated, not user input], server-side. A lifecycle event ignores your template entirely and renders a platform-written prompt instead, and it bypasses filter — silence must not be filterable by accident.

{{ message.text }} is hardcoded to an empty string on a manual fire, and {{ message.source }} to manual_test. A manual fire is not a way to inject test input into the prompt.

filter compares dotted paths as strings against the same payload the prompt sees. It exists to break loops. For example, a source that reports both sides of a conversation would otherwise re-fire the agent on its own reply.

Webhook signature

Fires on POST /v1/webhooks/projects/{projectId}/{slug}. Kortix checks the request in this order, with a constant-time comparison:

  1. HMAC signature — header X-Kortix-Signature: sha256=<hmac> (the sha256= prefix is optional) or the GitHub-compatible X-Hub-Signature-256. HMAC-SHA256 over the raw request body, using the secret named by secret_env.
  2. Static token, only when no signature header is present, for senders that cannot HMAC-sign a body. Send the secret as X-Kortix-Token: <secret>, Authorization: Bearer <secret>, or Authorization: Basic <base64(user:secret)> (the password half is the token).
Status Meaning
202 Signature or token valid. Body is { status: "fired" | "queued" | "deduped", session_id, ... }.
200 Valid, but skipped — the project is paused, or the delivery did not match filter.
400 Malformed project ID or slug in the URL.
401 Signature and token both missing or wrong.
404 Trigger not found, disabled, not a webhook, or the project is not active.
409 The signing secret is missing, inactive, unavailable, or does not authorize the connector consumer. The response includes a webhook_secret_* code and remediation.
500 Auth passed, but the session failed to fire.

Endpoints

Method + path Needs Notes
GET /v1/projects/{projectId}/triggers project.trigger.read Lists triggers, runtime state, and manifest parse errors. A bad entry appears in errors[]; it does not break the other triggers.
POST /v1/projects/{projectId}/triggers project.trigger.create Creates a trigger. Commits to the manifest directly.
PATCH /v1/projects/{projectId}/triggers/{slug} project.trigger.update Partial update, merged onto the current entry.
DELETE /v1/projects/{projectId}/triggers/{slug} project.trigger.delete Also clears the trigger’s runtime state.
PATCH /v1/projects/{projectId}/triggers/activation project.trigger.update Body { paused: boolean }. See Pause and resume.
POST /v1/projects/{projectId}/triggers/{slug}/fire project.trigger.fire Manual fire. The built-in project member role holds project.trigger.fire, so an ordinary member can fire a trigger.
POST /v1/webhooks/projects/{projectId}/{slug} signature or token Public URL, gated by the webhook secret.

Pause and resume

A project-level switch stops every trigger in the project at once, independent of each trigger’s own enabled field. While paused, the scheduler skips the project and inbound webhooks return 200 with { status: "skipped" } — no session fires. A manual fire still works. Use this when the same repository runs on two control planes (for example, dev and production) so cron does not fire twice. CLI: kortix triggers pause and kortix triggers resume. See CLI for the full kortix triggers command group.

Limits and reliability

  • The scheduler polls roughly every second (default 1,000 ms; configurable via KORTIX_TRIGGER_SCHEDULER_INTERVAL_MS). Cron precision is best-effort to the second, even though the expression has a seconds field.
  • Each project allows 3 triggered sessions provisioning at once, by default. The account’s plan-tier active-session cap can also apply. A fire past either limit returns queued (202) instead of failing, and runs once a slot frees up.
  • A manual or webhook fire has a 45-second timeout. Loading the manifest has a 30-second timeout.
  • A cron fire is keyed on the due schedule slot, so a fire that timed out but actually landed does not duplicate on retry. A webhook fire is keyed on the delivery ID header, or a hash of the body and signature when the sender sends no ID.

Was this page helpful?