Workflows cheat sheet
One-page reference. Bookmark this.
name: my-workflow
description: ...
enabled: true
tags: [team, domain]
version: "1"
triggers: [ ... ]
inputs: [ ... ]
consts: { ... }
outputs: [ ... ]
settings: { ... }
steps: [ ... ]
- required
- optional
- optional
- required only for composed workflows
- optional
- required
Full reference: Anatomy of a workflow.
triggers:
- type: manual
- type: scheduled
with:
every: "5m"
- type: alert
- type: workflows.failed
on:
condition: "event.workflow.name : 'critical-ingest-pipeline'"
- type: cases.caseCreated
on:
condition: 'event.owner: "securitySolution"'
- type: entityStore.entityRiskScoreChanged
on:
condition: 'event.direction: "increase" AND event.delta >= 25'
- type: ai.conversation.updated
on:
condition: 'event.templateId: "investigation" and event.eventTypes: "user_message"'
- or: rrule with freq DAILY/WEEKLY/MONTHLY
- requires rule Action attachment
- GA (9.5+), tech preview (9.4)
- tech preview (9.5+)
- tech preview (9.5+)
- tech preview (9.6+), opt-in templates only
The full cases trigger family also includes cases.caseUpdated, cases.caseStatusUpdated, cases.attachmentsAdded, and cases.commentsAdded. The entity store trigger family also includes entityStore.entityAssetCriticalityUpdated. The Elastic Agent Builder trigger family also includes ai.attachmentAdded, ai.attachmentUpdated, ai.attachmentDeleted, and ai.conversation.metadataUpdated. Refer to Event-driven triggers.
Minimum schedule interval: 1 minute. Refer to Triggers.
- name: my_step
type: some.step_type
connector-id: "my-connector"
if: "inputs.run_me : true"
foreach: "{{ some.array }}"
timeout: "30s"
on-failure:
retry:
max-attempts: 3
delay: "5s"
strategy: exponential
continue: true
with:
# step-specific parameters.
# Note: workflow.execute / workflow.executeAsync use `workflow-id` INSIDE `with`.
- unique within the workflow
- top-level, kebab-case (for connector and AI steps)
- step-level KQL guard
- step-level iteration
| Want to… | Use |
|---|---|
| Query Elasticsearch | elasticsearch.search, elasticsearch.esql.query |
| Write to Elasticsearch | elasticsearch.index, elasticsearch.bulk, elasticsearch.update |
| Manage cases | cases.createCase, cases.updateCase, cases.addComment, cases.addAlerts, cases.addAttachments, cases.pushCases |
| Manage alerts | security.setAlertStatus, security.setAlertTags, security.assignAlert (preferred); kibana.SetAlertsStatus, kibana.SetAlertTags (PascalCase) |
| Manage attacks | security.setAttackStatus, security.setAttackTags, security.assignAttack |
| Enable or disable detection rules | security.enableRule, security.disableRule |
| Manage asset criticality | entityStore.updateAssetCriticality |
| Call an API | http (with optional connector-id) |
| Call a service | <connector>.<action> (for example, slack.postMessage, jira.createIssue) |
| Branch | if, switch |
| Loop | foreach, while, loop.break, loop.continue |
| Run independent work at the same time | parallel |
| Fan out to independent executions | workflow.executeAsync (tech preview) |
| Pause | wait, waitForInput |
| Transform data | data.filter, data.map, data.aggregate, data.parseJson, data.regexExtract |
| Call AI | ai.prompt, ai.classify, ai.summarize, ai.agent |
| Call another workflow | workflow.execute (synchronous), workflow.executeAsync (asynchronous, tech preview) |
| Log | console |
Full list: Step type index.
"{{ expr }}"
"${{ expr }}"
- string interpolation
- raw-value (arrays, objects, booleans, numbers)
Top-10 patterns:
"{{ inputs.name }}"
"{{ steps.search.output.hits.total.value }}"
"{{ event.alerts[0].host.name }}"
"{{ foreach.item._id }}"
"{{ variables.threshold }}"
"{{ now | date: '%Y-%m-%d' }}"
"{{ steps.x.output.body | json_parse }}"
"{{ event | json }}"
"{{ event.alerts[0].host.name | default: 'unknown' }}"
"${{ event.alerts | where: 'severity', 'critical' }}"
- read an input
- read a step output
- read trigger payload
- inside foreach
- read a data.set variable
- formatted timestamp
- parse a JSON string
- serialize to JSON string
- fallback
- filter inline
Full reference: Liquid filters.
| Variable | Contains |
|---|---|
inputs.* |
Workflow inputs at runtime. |
consts.* |
Constants from workflow top. |
steps.<name>.output |
Output of a previous step. |
steps.<name>.error |
Error if that step failed (with on-failure: continue). |
event.* |
Trigger payload. |
execution.* |
Current execution metadata. |
workflow.* |
Workflow metadata. |
foreach.* |
Loop context: item, index, total, items. |
while.iteration |
Zero-based iteration counter inside a while loop. |
variables.* |
Variables set by data.set. |
now, kibanaUrl |
Standard helpers. |
Full reference: Context variables.
on-failure:
retry:
max-attempts: 3
delay: "5s"
strategy: exponential # or "fixed"
jitter: true
condition: "steps.self.error.status : 429"
continue: true
fallback: [ ... ]
# abort is the default when no on-failure is set
- KQL
- log and move on
- graceful degradation
Precedence: per-step on-failure > workflow-level settings.on-failure > abort.
For cross-workflow error handling (page on-call when another workflow fails), use the workflows.failed event-driven trigger.
Full reference: Pass data and handle errors.
Alert trigger needs rule Action attachment.
type: alertalone isn't enough. Attach the workflow to the rule's Actions.whiledefaults tomax-iterations: 2000withon-limit: continue. When the loop hits the cap, the step succeeds quietly. Seton-limit: failif you want the workflow to fail at the cap.- name: poll_status type: while condition: "steps.check.output.status != 'ready'" max-iterations: limit: 100 on-limit: fail steps: - name: check type: http with: url: "https://api.example.com/jobs/{{ inputs.job_id }}"- Fail the workflow at the cap; default is continue
switch.casesis an array, not a map. Each case is a{ case: <value>, steps: [...] }object. Refer toswitch.- name: route type: switch expression: "{{ steps.classify.output.severity }}" cases: - case: critical steps: - name: page_oncall type: pagerduty.triggerIncident with: { ... } - case: high steps: - name: notify_team type: slack.postMessage with: { ... } default: - name: log_only type: console with: message: "low severity"cases.*parameters usesnake_case:case_id, notcaseId.# Wrong - type: cases.addComment with: caseId: "{{ steps.create.output.case.id }}" # Right - type: cases.addComment with: case_id: "{{ steps.create.output.case.id }}" comment: "Investigation started."kibana.SetAlertsStatus/kibana.SetAlertTagsare PascalCase. Notkibana.set_alerts_status.- name: close_false_positive type: kibana.SetAlertsStatus with: signal_ids: ["{{ event.alerts[0]._id }}"] status: closed reason: "Verified non-malicious."- PascalCase step type
AI step identifiers are top-level kebab-case:
connector-id,agent-id,inference-id. Liquid expressions in these top-level fields aren't evaluated; use literal values. Refer to AI steps.# Wrong — fields nested under with, and templated agent-id is not substituted - type: ai.agent with: agentId: "{{ consts.agent_id }}" message: "..." # Right — top-level kebab-case with literal values - type: ai.agent agent-id: elastic-ai-agent with: message: "..."Composition's
workflow-idis kebab-case but lives insidewith. It's the one exception to the top-level-kebab-case pattern.- name: enrich type: workflow.execute with: workflow-id: "shared--enrich-alerts" inputs: alerts: "${{ event.alerts }}"- kebab-case, inside with
TipThe
shared--prefix in the workflow ID is an optional naming convention for shared workflows, not required syntax.data.*steps (exceptdata.set) put source data at the top level:items:,arrays:, orsource:. The transformation configuration goes inwith.- name: keep_critical type: data.filter items: "${{ steps.search.output.hits.hits }}" with: condition: "item._source.severity : 'critical'"- top-level source
Use
${{ ... }}for arrays and objects,{{ ... }}for strings.# Wrong — array is stringified - type: foreach foreach: "{{ event.alerts }}" # Right — raw-value form preserves the array - type: foreach foreach: "${{ event.alerts }}"to_jsondoesn't exist. Usejsonto serialize orjson_parseto parse.# Serialize an object to a JSON string payload: "{{ event.alerts[0] | json }}" # Parse a JSON string into an object parsed: "{{ steps.http.output.body | json_parse }}"data.filterandifconditions are KQL, not Liquid. Useitem._source.severity : 'critical', notitem._source.severity == 'critical'.# Wrong — Liquid comparison - type: data.filter items: "${{ steps.search.output.hits.hits }}" with: condition: "item._source.severity == 'critical'" # Right — KQL equality - type: data.filter items: "${{ steps.search.output.hits.hits }}" with: condition: "item._source.severity : 'critical'"
- Build your first workflow: Hands-on tutorial if you're new.
- Glossary: Definitions for every term used in this docset.
- Step type index: The A-Z lookup.
- Troubleshooting: When something isn't working.
elastic/workflowslibrary: 57 example workflows you can adapt.