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>.
| Trigger | Fires on | Needs |
|---|---|---|
schedule | cron expressions (minute-level — this is your machine, not CI) | — |
file-watch | files created, modified, or deleted under watched globs | — |
git | local repository events: commits, branch creation | a git repo |
run-completed | another workflow finishing (done or failed) | — |
manual | you, via lk run or the desktop app | — |
| GitHub events | issues, pull requests, and other repository events | Console |
slack | mentions and DMs to the shared Slack app | Console |
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:
| Trigger | Default | Why |
|---|---|---|
schedule | latest | missed cron ticks collapse into one |
file-watch | latest | a burst of saves is one change |
git | all | each commit is individually meaningful |
run-completed | all | skipping 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.