Skip to content

Event routing & generic webhooks

The workflows page shows all permanently registered workflows listening for non-push events — internal events, schedules, lifecycle hooks, and generic webhooks.

Each row shows:

  • Workflow name and trigger types.
  • Last trigger time.
  • Staleness indicators.

Use the filters to narrow by trigger type or repository.

The Run now button triggers a cron-scheduled workflow immediately without waiting for the next scheduled time.

The resulting run is recorded with a manual_schedule trigger type, so you can distinguish manual runs from automatic cron runs in the run history.

The button only appears on workflow rows that have a schedule trigger and are not disabled.

KiCI supports two categories of non-GitHub event processing: internal event routing (workflow chaining via custom and system events) and generic webhook ingestion (accepting HTTP webhooks from external services). Both integrate into the standard trigger matching pipeline — workflows declare triggers in TypeScript, and the orchestrator matches incoming events against lock file entries.

Internal events flow through the orchestrator’s event router:

  1. Emission — A running step calls ctx.emit('event-name', payload), which sends an event.emit WS message to the orchestrator
  2. Persistence — The orchestrator stores the event in the kici_events PostgreSQL table and issues a NOTIFY on the kici_event_channel channel
  3. Fan-out — All orchestrators in the cluster LISTEN on kici_event_channel and evaluate the event against cached lock file triggers
  4. Dispatch — Matched workflows are dispatched to agents via the normal job queue

System events (workflow_complete, job_complete) are emitted automatically by the orchestrator when executions finish. No opt-in is required.

The event router includes a circuit breaker to prevent event loops (e.g., Workflow A emits event X, Workflow B triggers on X and emits event Y, Workflow A triggers on Y):

  • Chain depth limit — Events carry a chain_depth counter incremented on each re-emission. Events exceeding the max depth (default: 10) are dropped.
  • Rate limiting — In-memory sliding window limits events per routing key per minute. Excess events are logged and dropped.
  • TTL cleanup — Events older than the configured TTL (default: 7 days) are periodically deleted.

Event routing defaults can be overridden via KICI_-prefixed environment variables, YAML config (eventRouter: section), or the shared DB config store. The 4-layer resolution chain applies: env var > YAML > DB > defaults.

SettingEnv varYAML pathDefaultDescription
maxChainDepthKICI_EVENT_ROUTER_MAX_CHAIN_DEPTHeventRouter.maxChainDepth10Maximum event chain depth before circuit breaker trips
rateLimitPerWorkflowPerMinuteKICI_EVENT_ROUTER_RATE_LIMIT_PER_WORKFLOW_PER_MINUTEeventRouter.rateLimitPerWorkflowPerMinute100Maximum events per event name per minute (note: keyed by event name, not workflow name, despite the config field name)
eventTtlSecondsKICI_EVENT_ROUTER_EVENT_TTL_SECONDSeventRouter.eventTtlSeconds604800Event retention in seconds (7 days)
cleanupIntervalMsKICI_EVENT_ROUTER_CLEANUP_INTERVAL_MSeventRouter.cleanupIntervalMs3600000Cleanup interval for expired events (1 hour)

Workflows with non-Git triggers (custom events, system events, cron schedules, generic webhooks, lifecycle hooks) must be registered in the orchestrator’s database before events arrive. Unlike Git-based triggers (push, PR) where the orchestrator fetches the lock file per-event using repo/ref from the webhook, internal events carry no repo/ref information. The orchestrator needs to know which workflows to evaluate before the event arrives.

Registration happens automatically when code is pushed to the default branch:

  1. A git push to the default branch arrives via webhook
  2. The orchestrator processes the push and fetches (or compiles) the lock file
  3. extractRegisterableWorkflows() identifies workflows with registerable trigger types
  4. The registration store atomically replaces all registrations for that customer+repo (DELETE + INSERT in a single transaction)
  5. The registry version is bumped, notifying cluster peers to reload their in-memory index
  6. All orchestrators in the cluster refresh their registration index if the version is newer

The following trigger types cause a workflow to be registered:

Trigger typeExample
kici_eventCustom events via ctx.emit()
workflow_completeTriggered when another workflow finishes
job_completeTriggered when a specific job finishes
generic_webhookExternal HTTP webhooks (ArgoCD, Jenkins, etc.)
scheduleCron-based schedules
lifecycleLifecycle events (startup, shutdown, etc.)

Workflows with only Git-provider triggers (push, PR, tag, etc.) are not registered — they use the standard per-event lock file pipeline.

kici-admin registration is the operator-facing way to inspect registrations; it wraps the orchestrator’s /api/v1/admin/registrations admin endpoints, so the equivalent raw curl is shown after each CLI command for scripting against the API directly. All endpoints require Bearer token authentication with the appropriate permission.

List registrations

Terminal window
# List all registrations
kici-admin registration list
# Filter by customer, repo, or trigger type
kici-admin registration list --org my-org
kici-admin registration list --org my-org --repo org/my-repo
kici-admin registration list --trigger-type schedule
Terminal window
# List all registrations
curl https://<orchestrator>/api/v1/admin/registrations \
-H "Authorization: Bearer <admin-token>"
# Filter by customer
curl https://<orchestrator>/api/v1/admin/registrations?customerId=my-org \
-H "Authorization: Bearer <admin-token>"
# Filter by customer and repo
curl "https://<orchestrator>/api/v1/admin/registrations?customerId=my-org&repoIdentifier=org/my-repo" \
-H "Authorization: Bearer <admin-token>"
# Filter by trigger type
curl https://<orchestrator>/api/v1/admin/registrations?triggerType=schedule \
-H "Authorization: Bearer <admin-token>"

Get single registration

Terminal window
kici-admin registration show <id>
Terminal window
curl https://<orchestrator>/api/v1/admin/registrations/<id> \
-H "Authorization: Bearer <admin-token>"

Force registry refresh

Bumps the registry version, triggering all cluster peers to reload registrations from the database. Useful after manual database changes or to force re-synchronization.

Terminal window
curl -X POST https://<orchestrator>/api/v1/admin/registrations/refresh \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"customerId": "my-org",
"repoIdentifier": "org/my-repo"
}'

Delete a registration

Deletes a single registration and bumps the registry version to notify peers.

Terminal window
curl -X DELETE https://<orchestrator>/api/v1/admin/registrations/<id> \
-H "Authorization: Bearer <admin-token>"
EndpointRequired permission
GET /api/v1/admin/registrationscontext.read
GET /api/v1/admin/registrations/:idcontext.read
POST /api/v1/admin/registrations/refreshcontext.update
DELETE /api/v1/admin/registrations/:idcontext.delete

Cron-triggered workflows are evaluated periodically by the orchestrator’s cron scheduler. Only the Raft leader evaluates schedules to prevent duplicate firings in multi-orchestrator clusters.

  1. Every 30 seconds (hardcoded), the leader queries the registration index for workflows with schedule triggers
  2. For each schedule, the croner library parses the cron expression and computes the most recent past scheduled time
  3. If that time is after the last-fired time (tracked in the cron_last_fired table), the schedule fires
  4. Firing emits a __schedule_fire internal event through the event router, which matches against registered workflows

When a new orchestrator becomes the Raft leader:

  1. The last-fired cache is loaded from the cron_last_fired database table
  2. Each registered schedule is evaluated once for recovery
  3. Missed schedules fire once (not once per missed interval)
  4. Normal periodic evaluation then starts

The cron scheduler has no operator-configurable environment variables. All defaults are hardcoded:

SettingValueDescription
Evaluation interval30 secondsHow often the leader checks schedules
Recovery behaviorFire onceOne fire per missed schedule on recovery
Cron parsercronerLibrary for cron expression parsing
Last-fired trackingPostgreSQLcron_last_fired table

Generic webhook sources allow the orchestrator to accept HTTP webhooks from non-GitHub services (ArgoCD, Jenkins, Grafana, Slack, or any HTTP-capable source).

External services send webhooks to:

  • Direct to orchestrator: POST https://<orchestrator>/webhook/<orgId>/generic/<sourceId>
  • Via Platform relay: POST https://<platform>/webhook/<orgId>/generic/<sourceId>

The orgId and sourceId are assigned when creating the source via the admin API.

Create a generic webhook source via the CLI or admin API:

CLI (recommended):

Terminal window
kici-admin source add generic \
--org my-org \
--name argocd-prod \
--verification hmac_sha256 \
--secret @/path/to/webhook-secret.txt \
--event-type-header X-ArgoCD-Event \
--rate-limit 60 \
--max-payload 1048576

REST API:

Terminal window
curl -X POST https://<orchestrator>/api/v1/admin/generic-sources \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"orgId": "my-org",
"name": "argocd-prod",
"verificationMethod": "hmac_sha256",
"verificationConfig": { "secret": "whsec_..." },
"eventTypeHeader": "X-ArgoCD-Event",
"rateLimitRpm": 60,
"maxPayloadBytes": 1048576
}'
FieldTypeRequiredDescription
customerIdstringyesCustomer or organization identifier
namestringyesHuman-readable source name (unique per customer)
verificationMethodenumnohmac_sha256, bearer_token, ip_allowlist, or none (default: none)
verificationConfigobjectnoMethod-specific config (see below)
eventTypeHeaderstringnoHTTP header for event type extraction (default: X-Event-Type)
eventTypePathstringnoJSONPath in payload for event type extraction
idempotencyKeyHeaderstringnoHTTP header for idempotency key
idempotencyKeyPathstringnoJSONPath in payload for idempotency key
dedupWindowSecondsintegernoDeduplication window in seconds
maxPayloadBytesintegernoMaximum payload size in bytes
allowedEventsstring[]noAllowlist of accepted event types
stripHeadersstring[]noHeaders to strip before passing to workflows
rateLimitRpmintegernoRate limit: requests per minute

HMAC-SHA256 — The source signs payloads with a shared secret. The orchestrator verifies the signature in the header configured via verificationConfig.headerName (default: x-signature-256). Both sha256= prefixed and raw hex formats are accepted.

{
"verificationMethod": "hmac_sha256",
"verificationConfig": { "secret": "whsec_your_shared_secret" }
}

To accept signatures from providers that use a different header name (for example GitHub’s X-Hub-Signature-256, Forgejo/Gitea’s X-Gitea-Signature, or Gogs’s X-Gogs-Signature), set headerName explicitly:

{
"verificationMethod": "hmac_sha256",
"verificationConfig": {
"secret": "whsec_your_shared_secret",
"headerName": "x-hub-signature-256"
}
}

The CLI (kici-admin source add generic) currently does not expose --signature-header; to customise the header name use the REST API form above (or kici-admin source update --verification hmac_sha256 --config '{...}' if you already created the source).

Bearer token — The source sends a static token in the Authorization: Bearer <token> header. Verification uses constant-time comparison.

{
"verificationMethod": "bearer_token",
"verificationConfig": { "token": "your_bearer_token" }
}

IP allowlist — Only requests from listed IP addresses are accepted.

{
"verificationMethod": "ip_allowlist",
"verificationConfig": { "allowedIps": ["10.0.0.1", "10.0.0.2"] }
}

None — No verification. Use only for trusted internal networks.

CLI:

Terminal window
# List sources (use --org to include generic sources)
kici-admin source list --org my-org
# Get details of a generic source
kici-admin source get <source-id>
# Update source config
kici-admin source update-generic <source-id> --rate-limit 120
# Disable source (stops accepting webhooks)
kici-admin source disable <source-id>
# Enable source
kici-admin source enable <source-id>
# Soft delete
kici-admin source remove <source-id> --generic --yes
# Hard delete (permanent)
kici-admin source remove <source-id> --generic --hard --yes

REST API:

Terminal window
# List sources for a customer
curl https://<orchestrator>/api/v1/admin/generic-sources?orgId=my-org \
-H "Authorization: Bearer <token>"
# Get source details
curl https://<orchestrator>/api/v1/admin/generic-sources/<id> \
-H "Authorization: Bearer <token>"
# Update source config
curl -X PATCH https://<orchestrator>/api/v1/admin/generic-sources/<id> \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "rateLimitRpm": 120 }'
# Disable source (stops accepting webhooks)
curl -X POST https://<orchestrator>/api/v1/admin/generic-sources/<id>/disable \
-H "Authorization: Bearer <token>"
# Enable source
curl -X POST https://<orchestrator>/api/v1/admin/generic-sources/<id>/enable \
-H "Authorization: Bearer <token>"
# Soft delete (can be restored)
curl -X DELETE https://<orchestrator>/api/v1/admin/generic-sources/<id> \
-H "Authorization: Bearer <token>"
# Hard delete (permanent)
curl -X DELETE https://<orchestrator>/api/v1/admin/generic-sources/<id>?hard=true \
-H "Authorization: Bearer <token>"

Universal-git sources (Forgejo, Gitea, Gogs, GitLab, plain GitHub)

Section titled “Universal-git sources (Forgejo, Gitea, Gogs, GitLab, plain GitHub)”

A generic source can be promoted to a universal-git source by passing --preset, --git-url-template, and credential flags to source add generic. This unlocks the full trigger pipeline for non-GitHub-App forges — push / pull_request matching, lock-file shallow-clone via PAT or SSH, and participation in the two-axis global-workflow policy. See the user guide for the full setup recipe.

Key differences from a plain generic source:

  • Webhook event header defaults from the preset. --preset forgejo auto-sets event_type_header = X-Gitea-Event, same for gitea / gogs / gitlab-repo / github-repo. Only --preset custom requires explicit --event-type-header.
  • Routing key stays generic:<orgId>:<sourceId>. Global workflow policy (kici-admin org-settings global-workflows ...) keys off this routing key exactly like any other source.
  • Cross-provider dispatch works out of the box. A global workflow authored in one universal-git source can fan out against pushes from a different source in the same org (including from a GitHub App source). The agent receives split sourceAuth + workflowAuth, each minted from the respective bundle.

When using the Platform relay, generic webhooks follow the same path as GitHub webhooks:

  1. External service POSTs to https://<platform>/webhook/<orgId>/generic/<sourceId>
  2. Platform verifies the signature (for HMAC sources) or passes through (for skip_verification sources)
  3. Platform relays via WebSocket to the orchestrator using routing key generic:<orgId>:<sourceId>
  4. Orchestrator processes the webhook through the normal pipeline

Sources using bearer token, IP allowlist, or no verification are automatically flagged as skip_verification in the Platform — the orchestrator handles verification instead.

By default, events emitted from one repository can only trigger workflows in the same repository. Cross-repo event delivery requires explicit trust relationships.

Trust relationships are always bidirectional — both repos must trust each other. Create a trust entry via the admin API:

Terminal window
curl -X POST https://<orchestrator>/api/v1/admin/trust \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"sourceRepo": "org/infra-repo",
"sourceRoutingKey": "github:42",
"targetRepo": "org/app-repo",
"targetRoutingKey": "github:42",
"allowedEvents": ["deploy-*", "release-*"]
}'
FieldTypeRequiredDescription
sourceRepostringyesRepository emitting events (e.g., org/infra-repo)
sourceRoutingKeystringyesRouting key of the source repo
targetRepostringyesRepository receiving events (e.g., org/app-repo)
targetRoutingKeystringyesRouting key of the target repo
allowedEventsstring[]noGlob patterns for allowed event names (default: all)

The allowedEvents field supports glob patterns (via picomatch) to restrict which events can cross repo boundaries. For example, ["deploy-*"] allows only events matching the deploy-* pattern.

Terminal window
# List trust entries for a routing key
curl https://<orchestrator>/api/v1/admin/trust?routingKey=github:42 \
-H "Authorization: Bearer <token>"
# Remove a trust relationship
curl -X DELETE https://<orchestrator>/api/v1/admin/trust/<id> \
-H "Authorization: Bearer <token>"