Triage alerts with an AI agent
This guide walks through building a workflow that triages Attack Discovery alerts with an Elastic Agent Builder agent. The workflow creates a case for each discovery, has an agent produce a remediation analysis, posts a concise summary to Slack, and isolates the host pending review.
The workflow is adapted from ad-automated-triaging.yaml in the elastic/workflows library.
If you're new to workflows, complete Build your first workflow first.
- Permissions.
Allon Analytics > Workflows, Security > Cases, and whatever Agent Builder privilege is required to invoke agents in your space. Refer to Kibana privileges. - Attack Discovery enabled. Attack Discovery must be running in your Elastic Security deployment and producing findings. Refer to Attack Discovery.
- Agent Builder agent. A configured agent in Elastic Agent Builder that can reason over security context. Use one of the built-in agents (for example, the Elastic AI Agent) or a custom agent. Note the agent ID.
- Slack connector. A Slack connector or webhook URL for notifications.
- Attach the workflow to a rule. After saving the workflow, attach it to the Attack Discovery detection rule or the rule group you want to triage. Refer to Alert triggers.
The workflow runs whenever the attached rule fires. Because Attack Discovery alerts batch multiple related alerts into a single finding, the workflow iterates with foreach over event.alerts and handles each discovery independently:
- Create a case populated with the discovery's attack summary, affected entities, and MITRE tactics.
- Attach the component alerts to the case so investigators can pivot.
- Call an agent to produce a remediation and response analysis. Attach the analysis to the case.
- Call an agent again to produce a short Slack-ready summary.
- Isolate the host pending review.
- Notify Slack with the summary, risk score, and links back to the case, host, and workflow execution.
-
Trigger on Attack Discovery alerts
triggers: - type: alert enabled: trueAttach the workflow to the Attack Discovery rule after saving so the rule invokes this workflow.
-
Iterate over each discovery
An Attack Discovery alert bundles multiple related findings. Loop over them so each discovery gets its own case:
steps: - name: for_each_discovery type: foreach foreach: event.alerts steps: # Per-discovery steps go here. Use foreach.item to access the current discovery.Inside the loop,
foreach.itemis the current discovery object, includingforeach.item.attack_discovery.title_with_replacements,foreach.item.attack_discovery.alert_ids, andforeach.item.risk_score. -
Open a case from the discovery
Populate the case body from the discovery's Markdown-formatted summary fields:
- name: create_case type: cases.createCase with: title: "[Attack Discovery] {{ foreach.item.attack_discovery.title_with_replacements }}" description: | ## Attack summary {{ foreach.item.attack_discovery.summary_markdown_with_replacements }} ## Detailed analysis {{ foreach.item.attack_discovery.details_markdown_with_replacements }} ## Affected entities {{ foreach.item.attack_discovery.entity_summary_markdown_with_replacements }} ## Investigation context - Discovery ID: {{ foreach.item.uuid }} - Risk score: {{ foreach.item.risk_score }} - Alert count: {{ foreach.item.attack_discovery.alerts_context_count }} owner: "securitySolution" severity: "high" tags: ["auto-triage", "attack-discovery"] -
Attach the component alerts
Loop over the alert IDs that make up the discovery and attach them:
- name: attach_alerts type: foreach foreach: foreach.item.attack_discovery.alert_ids steps: - name: add_alert type: cases.addAlerts with: case_id: "{{ steps.create_case.output.id }}" alerts: - alertId: "{{ foreach.item }}" index: ".alerts-security.alerts-default" rule: id: "{{ event.alerts[0].kibana.alert.rule.uuid }}" name: "{{ event.alerts[0].kibana.alert.rule.name }}" -
Ask an agent for a remediation plan
Call the agent with the discovery context and a specific prompt. The agent returns its analysis on
steps.triage.output.message:- name: triage type: ai.agent agent-id: "{{ consts.agent_id }}" connector-id: "{{ consts.connector_id }}" create-conversation: false with: prompt: | How should we remediate this attack? - Use your knowledge of Elastic Defend to generate remediation commands. - Include a confidence score for the true-positive ratio. - Do not render videos or gifs. - Do not include citations. <detected_attack> {{ foreach.item | json(2) }} </detected_attack>Then attach the agent's analysis to the case:
- name: add_analysis type: cases.addComment with: case_id: "{{ steps.create_case.output.id }}" comment: "{{ steps.triage.output.message }}" -
Ask an agent for a Slack-ready summary
Reuse the agent with a different prompt to get a one-to-two-sentence summary suitable for Slack:
- name: ai_summary type: ai.agent agent-id: "{{ consts.agent_id }}" connector-id: "{{ consts.connector_id }}" create-conversation: false with: prompt: | Produce a one-to-two-sentence summary of the attack below for a Slack notification. Wrap entity names like hostnames in backticks. Output only the summary, no preamble. <detected_attack> {{ foreach.item | json(2) }} </detected_attack> -
Isolate the affected host
Use
kibana.requestto call the endpoint isolation API and link the action to the case:- name: isolate_host type: kibana.request with: method: POST path: /api/endpoint/action/isolate body: endpoint_ids: - "{{ foreach.item.host.id }}" comment: "Automated isolation pending analyst review. Case {{ steps.create_case.output.id }}." case_ids: - "{{ steps.create_case.output.id }}"If your Attack Discovery payload doesn't carry
host.id, swap in a search step to look up the endpoint by hostname before isolation. -
Notify Slack
Post a rich Slack message with the AI summary, the risk score, and deep links back to the workflow artifacts:
- name: notify_slack type: http with: url: "{{ consts.slack_webhook }}" method: POST headers: content-type: application/json body: blocks: - type: header text: type: plain_text text: "Attack Discovery: {{ foreach.item.attack_discovery.title_with_replacements }}" - type: section text: type: mrkdwn text: "{{ steps.ai_summary.output.message }}" - type: actions elements: - type: button text: { type: plain_text, text: "View case" } url: "{{ kibanaUrl }}/app/security/cases/{{ steps.create_case.output.id }}" - type: button text: { type: plain_text, text: "View workflow run" } url: "{{ kibanaUrl }}/app/workflows/{{ workflow.id }}?executionId={{ execution.id }}&tab=executions" timeout: 30s
Full workflow YAML
name: security--ai-driven-triage
description: Triage Attack Discovery alerts with an AI agent. Open a case, attach analysis, isolate the host, and notify Slack.
enabled: true
tags: ["auto-triage", "attack-discovery"]
triggers:
- type: alert
enabled: true
consts:
agent_id: "your-agent-id"
connector_id: "your-connector-id"
slack_webhook: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
steps:
- name: for_each_discovery
type: foreach
foreach: event.alerts
steps:
- name: create_case
type: cases.createCase
with:
title: "[Attack Discovery] {{ foreach.item.attack_discovery.title_with_replacements }}"
description: |
## Attack summary
{{ foreach.item.attack_discovery.summary_markdown_with_replacements }}
## Detailed analysis
{{ foreach.item.attack_discovery.details_markdown_with_replacements }}
## Affected entities
{{ foreach.item.attack_discovery.entity_summary_markdown_with_replacements }}
## Investigation context
- Discovery ID: {{ foreach.item.uuid }}
- Risk score: {{ foreach.item.risk_score }}
- Alert count: {{ foreach.item.attack_discovery.alerts_context_count }}
owner: "securitySolution"
severity: "high"
tags: ["auto-triage", "attack-discovery"]
- name: attach_alerts
type: foreach
foreach: foreach.item.attack_discovery.alert_ids
steps:
- name: add_alert
type: cases.addAlerts
with:
case_id: "{{ steps.create_case.output.id }}"
alerts:
- alertId: "{{ foreach.item }}"
index: ".alerts-security.alerts-default"
rule:
id: "{{ event.alerts[0].kibana.alert.rule.uuid }}"
name: "{{ event.alerts[0].kibana.alert.rule.name }}"
- name: triage
type: ai.agent
agent-id: "{{ consts.agent_id }}"
connector-id: "{{ consts.connector_id }}"
create-conversation: false
with:
prompt: |
How should we remediate this attack? Reference Elastic Defend
remediation commands, include a confidence score, and do not
include citations or media.
<detected_attack>
{{ foreach.item | json(2) }}
</detected_attack>
- name: add_analysis
type: cases.addComment
with:
case_id: "{{ steps.create_case.output.id }}"
comment: "{{ steps.triage.output.message }}"
- name: ai_summary
type: ai.agent
agent-id: "{{ consts.agent_id }}"
connector-id: "{{ consts.connector_id }}"
create-conversation: false
with:
prompt: |
Produce a one-to-two-sentence summary of the attack below for a
Slack notification. Wrap hostnames in backticks. Output only the
summary.
<detected_attack>
{{ foreach.item | json(2) }}
</detected_attack>
- name: isolate_host
type: kibana.request
with:
method: POST
path: /api/endpoint/action/isolate
body:
endpoint_ids:
- "{{ foreach.item.host.id }}"
comment: "Automated isolation pending analyst review. Case {{ steps.create_case.output.id }}."
case_ids:
- "{{ steps.create_case.output.id }}"
- name: notify_slack
type: http
with:
url: "{{ consts.slack_webhook }}"
method: POST
headers:
content-type: application/json
body:
blocks:
- type: header
text:
type: plain_text
text: "Attack Discovery: {{ foreach.item.attack_discovery.title_with_replacements }}"
- type: section
text:
type: mrkdwn
text: "{{ steps.ai_summary.output.message }}"
- type: actions
elements:
- type: button
text: { type: plain_text, text: "View case" }
url: "{{ kibanaUrl }}/app/security/cases/{{ steps.create_case.output.id }}"
timeout: 30s
- Gate the isolation on a confidence threshold. Wrap
isolate_hostin anifstep that reads the confidence score from the agent's analysis and only isolates above a threshold. - Route by MITRE tactic. Use a
switchstep onforeach.item.attack_discovery.mitre_attack_tacticsto run different remediation branches per tactic. - Compose a shared enrichment workflow. Extract the "enrich + analyze" sequence into a child workflow and call it from multiple parent workflows that need similar analysis.
- Use
ai.summarizeinstead. If you don't need agent reasoning, replace the secondai.agentcall withai.summarizefor a simpler, cheaper summary.
- AI steps reference: Parameters for
ai.agent,ai.classify,ai.summarize, andai.prompt. - Call Elastic Agent Builder agents from Elastic Workflows: How to wire agents into workflow steps.
- Cases action steps: Full reference for
cases.*steps. - Attack Discovery: What Attack Discovery produces and how to enable it.
elastic/workflowslibrary: More agentic and SOC automation examples.