Connect & automateGithubEdit on GitHub

Triggers

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

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

Trigger types

Kortix supports two trigger types. There is no third type.

  • cron — runs on a schedule you set.
  • webhook — runs when an external service sends a signed request to the project's webhook URL.

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.

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", 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

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

Fields

FieldRequiredDefaultNotes
slugyes[a-z0-9][a-z0-9_-]{0,127}, unique per project.
typeyescron or webhook.
promptyesTemplate string. Renders as the session's first message.
namenoslugHuman label.
agentnodefaultMust name a key in agents:, or fall back to default_agent.
modelnoresolves at fire timeWire form provider/model, for example anthropic/claude-sonnet-4-5.
enablednotrueWhen false, the scheduler and the webhook receiver skip the entry.
session_modenofreshSee Session strategy.
session_idrequired for pinnedExact session to re-prompt.
session_keyrequired for keyedTemplate string. Setting it alone implies session_mode: keyed.
filternoDotted path → expected string. A webhook delivery that does not match returns 200 and fires no session.
cronone of cron/run_at, on type: cron6-field expression: second minute hour day month weekday.
run_atone of cron/run_at, on type: cronISO-8601 timestamp. Fires once, then stays dormant.
timezoneno, cron onlyUTCIANA name.
secret_envrequired, webhook onlyName of a project secret holding the webhook signing key.

A cron trigger needs cron or run_at, never both. A webhook trigger without secret_env is rejected — there is no unauthenticated webhook.

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.

Every trigger-fired session is created with project visibility (any project member can see it), not private to whoever configured or fired the trigger.

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.

SourceVariables
cron{{ cron.schedule }}, {{ cron.timezone }}, {{ cron.fired_at }}, {{ cron.last_fired_at }}. 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 }}.
manual (dashboard "fire now" or the fire endpoint){{ fired_at }}, {{ source }} (manual), {{ actor }}, {{ message.text }}, {{ message.source }}.

{{ 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).
StatusMeaning
202Signature or token valid. Body is { status: "fired" | "queued" | "deduped", session_id, ... }.
200Valid, but skipped — the project is paused, or the delivery did not match filter.
400Malformed project ID or slug in the URL.
401Signature and token both missing or wrong.
404Trigger not found, disabled, not a webhook, or the project is not active.
409secret_env has no value set.
500Auth passed, but the session failed to fire.

Endpoints

Method + pathNeedsNotes
GET /v1/projects/{projectId}/triggersproject.trigger.readLists triggers, runtime state, and manifest parse errors. A bad entry appears in errors[]; it does not break the other triggers.
POST /v1/projects/{projectId}/triggersproject.trigger.createCreates a trigger. Commits to the manifest directly.
PATCH /v1/projects/{projectId}/triggers/{slug}project.trigger.updatePartial update, merged onto the current entry.
DELETE /v1/projects/{projectId}/triggers/{slug}project.trigger.deleteAlso clears the trigger's runtime state.
PATCH /v1/projects/{projectId}/triggers/activationproject.trigger.updateBody { paused: boolean }. See Pause and resume.
POST /v1/projects/{projectId}/triggers/{slug}/fireproject.trigger.fireManual fire. Any project member can fire a trigger — this permission does not require the editor or manager role.
POST /v1/webhooks/projects/{projectId}/{slug}signature or tokenPublic 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 every 60 seconds. Cron precision is best-effort to the minute, 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.
Triggers – Kortix Docs