Triggers

A trigger decides when a workflow fires. Workflows declare them under on:; one workflow can have several, and each can be switched off without touching the others. Whatever the trigger, you can always start a workflow by hand — ▶ Run in the app, or lk run <workflow>.

TriggerFires onNeeds
schedulecron expressions (minute-level — this is your machine, not CI)
file-watchfiles created, modified, or deleted under watched globs
gitlocal repository events: commits, branch creationa git repo
run-completedanother workflow finishing (done or failed)
manualyou, via lk run or the desktop app
GitHub eventsissues, pull requests, and other repository eventsConsole
slackmentions and DMs to the shared Slack appConsole

schedule

on:
  schedule:
    - cron: "*/15 * * * *"

Aliases like daily, hourly, and weekly are accepted, matching GitHub Agentic Workflows.

file-watch

on:
  file-watch:
    paths: ["inbox/**/*.pdf"]
    events: [create, modify] # create | modify | delete
    debounce_ms: 2000 # collapse a burst of saves into one firing
    min_size_kb: 1 # reject empty or half-written files

git

on:
  git:
    events: [commit, branch-created]
    branch: ["main", "release/*"] # glob
    author_not: ["loopkeep[bot]"] # don't re-fire on your own automated commits

These are local repository events — no forge, no webhook. author_not is the standard guard against a workflow triggering itself with its own commits.

run-completed

on:
  run-completed:
    workflow: "test-fixer"
    status: [failed] # done | failed
    if: event.stats.attempts >= 3 # optional narrowing

Subscribes to another workflow finishing. The upstream run's structured output is available to the if: filter as event.output and is injected into the new run's trigger context.

Remote triggers (GitHub and Slack)

GitHub repository events and Slack mentions reach your daemon as wake signals through the Console. The signal carries no content — the daemon verifies the event against the GitHub or Slack API with your credentials and only then fires, so a forged signal can't start a run. Slack triggers default to ignoring bot messages (from_not: ["*bot*"]) to prevent reply loops. Setup lives in The Console.

Filters

Structured filters (the per-trigger keys above) handle most conditions. For the rest there's if:, a deliberately small expression language: event. paths, comparisons (== != > >= < <=), contains, && || !, and parentheses. No function calls, no code execution — evaluation is deterministic. A firing rejected by a filter is still recorded (with filtered: true), so you can always see why something didn't run.

Coalescing

If your machine was asleep through three nightly firings, running the backlog three times in the morning is rarely what you want. Each trigger type has a default:

TriggerDefaultWhy
schedulelatestmissed cron ticks collapse into one
file-watchlatesta burst of saves is one change
gitalleach commit is individually meaningful
run-completedallskipping a link breaks the chain

Override per trigger with coalesce: latest or coalesce: all. Coalesced firings record how many events were folded in.

Chains have a depth limit

run-completed and dispatch-workflow firings inherit their parent run's chain depth plus one, capped at 5. Deeper firings are refused and audited. This is the entire loop-prevention model — there is no DAG engine to configure, just a depth limit that stops runaway self-triggering.

Turning things off

Toggle a single workflow's automatic triggers with the switch on its Workflows card. Project Settings → Stop all triggers pauses one project's automatic firing until you resume it on the Policies screen, and Settings → Pause all automation is the global kill switch across every project.

Both act on future firings only: manual runs and runs already in flight keep going. The global pause is independent of trust — it doesn't touch any workspace's trust state.