Event system
KiCI supports two broad categories of workflow triggers: git-based triggers that work immediately, and event-based triggers that use a registration model. Understanding this distinction is key to working effectively with non-git triggers like schedules, custom events, and generic webhooks.
Overview
Section titled “Overview”Git-based triggers (push(), pr(), tag(), comment(), review(), release(), etc.) work immediately after you commit your lock file. When a GitHub webhook arrives, the orchestrator fetches your lock file and evaluates triggers on the spot — no advance setup needed.
Event-based triggers work differently. The orchestrator needs to know about them before the event arrives. This is because event-based triggers are matched against a pre-built registration index rather than being evaluated per-event from a lock file fetch. The six event-based trigger types are:
kiciEvent()— custom events emitted from workflow stepsworkflowComplete()— fires when a workflow finishesjobComplete()— fires when a specific job finishesgenericWebhook()— HTTP webhooks from external servicesschedule()— cron-based time triggerslifecycle()— orchestrator lifecycle events (workflow completion, job failure, registration updates)
All six require the registration model to function — covered in detail below.
Event types
Section titled “Event types”Custom events
Section titled “Custom events”Custom events are user-defined events emitted from workflow steps using ctx.emit(). Use kiciEvent() to listen for them.
import { kiciEvent } from '@kici-dev/sdk';
// Listen for a custom event by namekiciEvent({ name: 'deploy-complete' });
// With payload matching (JSONPath)kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } });
// With negative filterkiciEvent({ name: 'deploy-complete', not: { '$.env': 'staging' } });
// From a specific repositorykiciEvent({ name: 'deploy-complete', source: 'org/infra-repo' });Config options: name (required), match, not, source, description.
System events
Section titled “System events”The orchestrator automatically emits completion events when workflows and jobs finish. No manual emission needed — these fire automatically.
Workflow completion:
import { workflowComplete } from '@kici-dev/sdk';
// Any workflow completionworkflowComplete();
// Specific workflow by nameworkflowComplete({ name: 'build' });
// Only successful completionsworkflowComplete({ name: 'build', status: ['success'] });Config options: name, status ('success', 'failed', 'cancelled'), source, description.
Job completion:
import { jobComplete } from '@kici-dev/sdk';
// Any job completionjobComplete();
// Specific workflow + jobjobComplete({ workflow: 'build', job: 'test' });
// Only failuresjobComplete({ workflow: 'build', job: 'test', status: ['failed'] });Config options: workflow, job, status ('success', 'failed', 'cancelled', 'skipped'), source, description.
External events
Section titled “External events”Generic webhooks let you trigger workflows from any HTTP service — Stripe, ArgoCD, Slack, Grafana, or your own internal services.
import { genericWebhook } from '@kici-dev/sdk';
// Match any event from a sourcegenericWebhook({ source: 'stripe' });
// Match specific event typesgenericWebhook({ source: 'stripe', events: ['invoice.paid'] });
// With HMAC-SHA256 signature verificationgenericWebhook({ source: 'stripe', events: ['invoice.paid'], auth: { method: 'hmac-sha256', secret: 'stripe-signing-key', signatureHeader: 'stripe-signature', },});
// With API key authgenericWebhook({ source: 'slack', auth: { method: 'api-key', secret: 'slack-token' },});Config options: source (required), events, match, not, auth, path, description.
The source field MUST match the --name that an operator passed to kici-admin source add generic --name <name> when the source was created — that string is the source’s identifier in the orchestrator. Generic webhook sources must be created by an operator before events can be received; see Operator guide: event routing for setup instructions.
Schedule events
Section titled “Schedule events”Cron-based triggers evaluated by the orchestrator on a periodic interval. Only the Raft leader evaluates schedules in a clustered deployment.
import { schedule } from '@kici-dev/sdk';
// Run every hourschedule({ cron: '0 * * * *' });
// Run daily at 2 AM UTCschedule({ cron: '0 2 * * *' });
// Run weekly on Mondays at 9 AM Easternschedule({ cron: '0 9 * * 1', timezone: 'America/New_York' });Config options: cron (required), timezone (defaults to 'UTC'), description.
Lifecycle events
Section titled “Lifecycle events”Lifecycle triggers listen for orchestrator-level events related to workflow execution and system state changes.
import { lifecycle } from '@kici-dev/sdk';
// Trigger when any workflow completeslifecycle({ events: ['workflow_complete'] });
// Trigger on job failures from a specific repolifecycle({ events: ['job_failed'], sources: ['org/deploy-repo'] });
// Trigger when registrations are updatedlifecycle({ events: ['registration_updated'] });Available events: 'workflow_complete', 'job_complete', 'job_failed', 'registration_updated'.
Config options: events (required), sources, description.
The registration model
Section titled “The registration model”This is the most important concept for understanding event-based triggers.
Why registrations exist
Section titled “Why registrations exist”When a GitHub webhook arrives (push, PR, etc.), the orchestrator fetches your lock file from the repository and evaluates triggers on the spot. This works because the event itself tells the orchestrator which repository to look at.
Event-based triggers are different. When a cron timer fires or a custom event is emitted, there is no incoming webhook pointing to a specific repository. The orchestrator needs to know in advance which workflows care about which events. That is what the registration model provides: a pre-built index of event-based workflows.
How registration works
Section titled “How registration works”- You define a workflow with an event-based trigger (e.g.,
schedule(),kiciEvent(),genericWebhook()) - You compile the workflow (
kici compile), which produces a lock file - You push the lock file to your repository’s default branch (e.g.,
mainormaster) - The orchestrator receives the push webhook, detects it targets the default branch, and extracts all workflows with event-based triggers from the lock file
- Those workflows are stored in the orchestrator’s registration database
- From that point on, matching events will trigger those workflows
Key implications
Section titled “Key implications”-
Event-based workflows do not trigger until you push to the default branch. If you add a new
schedule()workflow, it will not start running until you merge to your default branch. This is by design — the orchestrator cannot match events to workflows it does not know about. -
Registration is automatic. There is no manual setup. Push your code, and the orchestrator handles the rest.
-
Registrations refresh on every default-branch push. If you add, remove, or modify event-based workflows and push to the default branch, the orchestrator updates its registration index automatically. Removed workflows stop triggering. New workflows start triggering.
-
Git-based triggers are unaffected. Triggers like
push(),pr(), andtag()do not use registrations. They work immediately from any branch because the orchestrator evaluates them per-event from the lock file.
Practical example
Section titled “Practical example”You create a nightly build workflow:
import { workflow, job, step, schedule } from '@kici-dev/sdk';
export default workflow('nightly-build', { on: schedule({ cron: '0 2 * * *' }), jobs: [ job('build', { runsOn: 'linux', steps: [ step('build', async ({ $ }) => { await $`pnpm build`; }), ], }), ],});You compile it, commit the lock file, and push to a feature branch. Nothing happens — the cron will not fire because the orchestrator has not registered this workflow yet.
You merge the feature branch into main. On the merge push, the orchestrator extracts the nightly-build workflow (it has a ScheduleTrigger) and registers it. Starting at the next 2 AM UTC, the workflow will trigger.
How events are matched
Section titled “How events are matched”When an event arrives, the orchestrator follows this flow:
- Event received — a custom event is emitted by a step, a cron timer fires, or a generic webhook arrives
- Registration lookup — the orchestrator queries its registration index for workflows matching the event type (e.g., all workflows with
ScheduleTriggerfor a cron fire, or all workflows withKiciEventTriggerfor a custom event) - Trigger evaluation — for each candidate workflow, the orchestrator evaluates the trigger conditions: event name patterns, payload matching, status filters, source filters
- Dispatch — matched workflows are dispatched to agents for execution, following the same job queue and agent routing as git-triggered workflows
This lookup is fast because the registration index is held in memory and refreshed only when the registry version changes (on default-branch pushes).
Cross-source webhook delivery
Section titled “Cross-source webhook delivery”The catch-all webhook() trigger (see SDK reference: webhook()) participates in this same registration lookup, but with one twist: it fires for matching events arriving via any inbound webhook source in the same org, not just the source the workflow’s repo is bound to. The orchestrator maintains a (customerId, eventName) index over webhook trigger registrations and consults it on every inbound generic webhook.
The lookup is structurally org-isolated — a generic webhook delivered to org A can never reach a workflow registered against org B, because foreign-org rows live in a different bucket of the index. When a webhook fires across sources, the runtime clone token, repo URL, and check-status posting all come from the registration’s source bundle, not the inbound source. The inbound source contributes only the event payload.
Circuit breaker
Section titled “Circuit breaker”Events can trigger workflows that emit more events, creating chains. The circuit breaker prevents runaway event storms.
Chain depth limit
Section titled “Chain depth limit”Each event carries a chainDepth counter. When a workflow triggered by an event emits a new event, the new event’s chain depth increments. The orchestrator rejects events that exceed the maximum chain depth.
- Default limit: 10 levels deep
- What happens when hit: the event is dropped and logged. It is not queued for later delivery.
For example: Workflow A emits event X (depth 0) -> Workflow B triggers, emits event Y (depth 1) -> … -> at depth 10, any further emitted events are dropped.
Rate limiting
Section titled “Rate limiting”Each workflow is rate-limited on how many events it can process per minute, using a sliding window.
- Default limit: 100 events per workflow per minute
- What happens when hit: additional events for that workflow are dropped and logged until the window clears.
These defaults are hardcoded in the orchestrator and are not currently configurable via environment variables.
Delivery guarantees
Section titled “Delivery guarantees”KiCI’s event router delivers every accepted event with at-least-once semantics:
- An event that passes the circuit breaker (chain depth + rate limit) and commits
to the
kici_eventstable is guaranteed to dispatch to all matching workflows at least once. - Each dispatch attempt acquires a short-lived lease (default 60 s) on the row. If the dispatching node crashes or the handler throws, the lease expires (or is released on failure) and the event is automatically retried.
- The retry policy is exponential backoff with full jitter: base 5 s, cap 5 min,
up to 5 attempts before the event lands in the DLQ (dead-letter queue).
Operators triage DLQ entries via
kici-admin event-dlq list / count / retry / discard.
What this means for workflow authors:
- Make event handlers idempotent. A retried dispatch may run a handler more than once (e.g. if the first attempt threw after a partial side-effect). Workflows that mutate external state should use idempotency keys, conditional writes, or other deduplication patterns — same advice as for any distributed CI system.
- Schedule fires are at-least-once too. A cron schedule that fires while a
leader is being killed will commit (atomically with
cron_last_fired) or roll back together — never half. Recovery on the new leader does not backfill multiple missed instants; if your workflow needs at-least-N guarantees across outages, drive it from a different mechanism (e.g. a workflow that runs more frequently and emits its own custom event). - Drops are still possible — and visible. Events rejected by the circuit
breaker (chain depth or rate limit exceeded) are dropped and logged, not
retried. That’s a deliberate safety mechanism; the metric to watch is
kici_orch_events_dropped_total{reason}.
Emitting custom events
Section titled “Emitting custom events”Custom events are emitted from workflow steps using ctx.emit(). You can optionally define typed event schemas using defineEvent().
Basic emission
Section titled “Basic emission”import { workflow, job, step, push } from '@kici-dev/sdk';
export default workflow('build', { on: push({ branches: 'main' }), jobs: [ job('build', { runsOn: 'linux', steps: [ step('build', async ({ $ }) => { await $`pnpm build`; }), step('notify', async (ctx) => { await ctx.emit('build-complete', { version: '1.0.0', success: true, }); }), ], }), ],});Typed event definitions
Section titled “Typed event definitions”Use defineEvent() with Zod schemas to create a typed contract for event payloads:
import { defineEvent, z } from '@kici-dev/sdk';
export const deployComplete = defineEvent( 'deploy-complete', z.object({ env: z.string(), version: z.string(), services: z.array(z.string()), }),);Then emit using the definition’s name:
step('emit', async (ctx) => { await ctx.emit(deployComplete.name, { env: 'prod', version: '1.2.3', services: ['api', 'web'], });});And consume in another workflow:
import { workflow, job, step, kiciEvent } from '@kici-dev/sdk';
export default workflow('post-deploy', { on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }), jobs: [ job('smoke-test', { runsOn: 'linux', steps: [ step('test', async ({ $ }) => { await $`./scripts/smoke-test.sh`; }), ], }), ],});Custom events are delivered immediately when emitted (mid-workflow, not queued until workflow completion). See the SDK reference: emitting events section for the full ctx.emit() API.
See also
Section titled “See also”- SDK reference: event triggers — complete API signatures for all trigger builders
- SDK reference: emitting events —
ctx.emit()anddefineEvent()API - Workflow patterns: workflow chaining — examples of event-driven workflow chains
- Operator guide: event routing — configuring generic webhook sources, trust relationships, and event routing
- Architecture: event system — internal event routing design, registration model, cluster synchronization