| name | workflows-custom-triggers |
| description | Register and implement custom workflow triggers from an external Kibana plugin using `@kbn/workflows-extensions`. Use when adding or modifying an event-driven trigger with `registerTriggerDefinition`, designing `eventSchema` Zod schemas, writing `documentation` and KQL `snippets`, wiring `emitEvent` via request context or `getClient`, choosing sync vs async public loader registration, updating `APPROVED_TRIGGER_DEFINITIONS`, or reviewing PRs that touch any of these. Always ask for the user's plugin id first to locate the correct plugin and file paths. |
Workflows — Custom Trigger Registration
Custom triggers let workflows subscribe to domain events from your plugin. A misconfigured trigger can break workflows on every restart, emit invalid payloads that fail at runtime, cause infinite event chains, or silently fail the approval gate. The defaults are not always right — verify each field below explicitly.
Canonical guide — read this for implementation steps
Follow the end-to-end steps, code templates, emit patterns, guardrails, and approval workflow in:
dev_docs/TRIGGERS.md
Sections to use while implementing:
Overview
A custom workflow trigger is owned and registered by a plugin other than workflows_extensions. The workflows-team plugin only hosts internal triggers; everything external must live in the owning plugin.
A trigger lives in three layers:
- Common —
id, eventSchema, title, description, optional documentation / snippets. Imported by both server and public to keep them in sync.
- Server — registers the same common definition via
registerTriggerDefinition; emits events with emitEvent.
- Public — spreads the common definition and adds icon only (browser-only UI).
Both sides register through the workflowsExtensions setup contract:
- Server:
registerTriggerDefinition(commonDefinition)
- Public:
registerTriggerDefinition(definition | () => Promise<definition>)
Additional references:
- Worked example:
examples/workflows_extensions_example/
- Common base type:
src/platform/plugins/shared/workflows_extensions/common/trigger_registry/types.ts (CommonTriggerDefinition, TriggerDocumentation, TriggerSnippets)
- Public trigger types:
src/platform/plugins/shared/workflows_extensions/public/trigger_registry/types.ts (PublicTriggerDefinition)
- Emit API:
src/platform/packages/shared/kbn-workflows/server/types.ts (WorkflowsClient, WorkflowsApiRequestHandlerContext, emitEvent)
- Event-chain guardrails:
src/platform/plugins/shared/workflows_extensions/server/event_chain_context.ts
- Approval fixture:
src/platform/plugins/shared/workflows_extensions/test/scout/api/fixtures/approved_trigger_definitions.ts
0. Locate the owning plugin (do this first)
Before creating or editing any files, ask the user for their plugin id (plugin.id from kibana.jsonc, camelCase — e.g. cases, workflowsExtensionsExample). Do not guess or assume a plugin.
If the user already named their plugin in the request, confirm it matches plugin.id before proceeding.
Resolve the plugin root
Find the plugin directory by searching for the id in kibana.jsonc:
rg '"id": "<pluginId>"' --glob '**/kibana.jsonc'
The directory containing that kibana.jsonc is the plugin root. Read it to confirm plugin.server / plugin.browser and whether workflowsExtensions is already in requiredPlugins.
Choose file locations inside the plugin
Inspect the plugin root for existing trigger or workflow extension layout. Follow conventions already used in that plugin rather than inventing new paths.
| If the plugin already has… | Add files there |
|---|
common/workflows/triggers/ (e.g. cases) | common/workflows/triggers/<trigger_name>.ts |
common/triggers/ (e.g. example plugin) | common/triggers/<trigger_name>.ts |
| No trigger files yet | Use common/triggers/<trigger_name>.ts and mirror under server/triggers/ and public/triggers/ |
Also check for existing registration hooks:
server/**/triggers/index.ts or server/plugin.ts — server registration
public/**/triggers/ or public/plugin.ts — public registration
Wire new triggers into those existing index/setup files when present; only create new index files when the plugin has no trigger layout yet.
Derive the trigger namespace
Trigger ids use <namespace>.<event> (kebab-case namespace, camelCase event). Prefer the namespace already used by that plugin's triggers. If none exist, derive kebab-case from plugin.id (e.g. workflowsExtensionsExample → workflows-extensions-example) and confirm with the user if ambiguous.
File layout
Mirror the layout used by examples/workflows_extensions_example/ so reviewers and the workflows team can find things:
your-plugin/
├── common/triggers/<trigger_name>.ts # common definition (id + eventSchema + title + description)
├── server/triggers/index.ts # registerTriggerDefinitions(setup)
├── public/triggers/<trigger_name>.ts # spread common + icon
└── public/triggers/index.ts # registerTriggerDefinitions(setup)
Keep id, eventSchema, title, description, documentation, and snippets in the common file only. Re-importing them on the public side is how server/public stay locked together.
Agent-specific rules (beyond TRIGGERS.md)
These are easy to miss during implementation or review — they are not always spelled out in the contributing doc:
| Concern | Rule |
|---|
id namespace | cases.* is owned by the Cases plugin — do not add new cases.* triggers outside that plugin |
| Server registration | Setup phase only — never call registerTriggerDefinition from start() |
| Public icon | Must be a React component via React.lazy from @elastic/eui/es/components/icon/assets/*. EUI icon name strings ('star') are not supported — the build will not fail, the icon will simply be missing |
| Public async loader | Prefer async import to keep zod out of the main bundle. Loaders that reject are caught and logged; one broken loader does NOT prevent other triggers from registering — verify the log when a trigger is silently missing |
| Public loader skip | Unlike step loaders, trigger loaders do not support returning undefined to skip registration. Guard optional triggers by not calling registerTriggerDefinition |
emitEvent request | Always pass the same request used for attribution/space so event-chain depth tracking works. Emitting without a real request when guardrails matter is an anti-pattern |
| Loop demo | Use kibana.request (not a generic HTTP connector) so event-chain headers propagate |
For naming conventions, schema rules, registration templates, emit patterns, and approval steps, follow TRIGGERS.md.
Quick rule reference
| Concern | Rule | Default if omitted | When wrong |
|---|
id | <kebab-namespace>.<camelEvent>; stable for the life of the workflow | n/a | Renames break user YAML |
| Common file | Holds id, eventSchema, title, description, documentation, snippets | n/a | Drift between server and public |
eventSchema | Zod object; .describe() on every field | n/a | Emit throws; bad editor/agent docs |
documentation.examples | Only fields from eventSchema | none | Agents generate invalid YAML |
snippets.condition | Valid KQL on event.* only | none | Registration fails or bad UX |
| Server registration | Register common definition in setup() | n/a | Trigger missing at runtime |
| Public registration | Spread common + React.lazy icon | n/a | Missing icon; duplicated strings |
emitEvent | Payload matches eventSchema; use real request | n/a | Throws; broken chain guardrails |
| Public loader | Async import to keep zod out of main bundle | sync inline | Trigger module inflates plugin bundle |
APPROVED_TRIGGER_DEFINITIONS | Add new ID + schemaHash; sorted by ID | test fails | CI blocks merge until updated |
Author checklist
When adding a new trigger:
-
Plugin location
-
Common file (common/triggers/<trigger>.ts or plugin convention) — see TRIGGERS.md Step 1
-
Server — see TRIGGERS.md Step 2
-
Public — see TRIGGERS.md Step 3
-
Plugin wiring
-
Approval gate — see TRIGGERS.md approval process
Reviewer checklist
When reviewing a PR that adds or modifies a custom trigger:
Reference implementations
| Plugin | Path | Notable pattern |
|---|
| Workflows example | examples/workflows_extensions_example/ | Canonical layout; async public loaders; custom_trigger docs/snippets; loop_trigger + emit route for chain-depth demo |
| Cases | x-pack/platform/plugins/shared/cases/common/workflows/triggers/ | Multiple triggers in one module; shared base schema; rich i18n documentation examples |
| Cases (public) | x-pack/platform/plugins/shared/cases/public/workflows/triggers/ | Spread common + lazy icon per trigger |
| Alerting v2 | x-pack/platform/plugins/shared/alerting_v2/server/lib/workflow_extensions/register_trigger_definitions.ts | Single registration loop over a catalog shared with runtime emit mapping |
Additional resources
- Contributing guide (canonical): dev_docs/TRIGGERS.md
- Public Slack channel for questions:
#one-workflow