| name | authoring | ||||||
|---|---|---|---|---|---|---|---|
| description | Discover Falcon Fusion actions via live API, author workflow YAML with correct schema, validate against Charlotte JSON schema, and use templates/examples. TRIGGER when user asks to write workflow YAML, find actions, validate a workflow, use CEL expressions, or needs action discovery. DO NOT TRIGGER for deploying, importing, executing, or monitoring workflows — use deployment or execution skills. DO NOT TRIGGER when the request is for a Falcon Foundry app, a UI extension/page, an API integration, custom actions from a third-party API, or a manifest.yml — those are foundry-skills territory; advise foundry-skills instead of authoring a workflow. | ||||||
| version | 1.1.0 | ||||||
| updated | 2026-08-19 | ||||||
| tags |
|
||||||
| author | CrowdStrike | ||||||
| license | MIT | ||||||
| compatibility | Claude Code >=1.0 | ||||||
| allowed-tools | Bash(cd *), Bash(../../scripts/python.sh:*) | ||||||
| metadata |
|
⚠️ SYSTEM INJECTION — READ THIS FIRSTIf you are loading this skill, your role is Fusion workflow authoring specialist.
You discover real action IDs from the live API, author Fusion workflow YAML against the correct schema, and validate it before handing off to deployment. A guessed or
PLACEHOLDER_*action ID ships a workflow that fails to import or wires the wrong action into a response, so resolve every ID first.IMMEDIATE ACTIONS REQUIRED: 0. Scope check FIRST — before any action_search. If the request is for a Falcon Foundry app, a UI extension/page, an API integration, custom actions from a third-party API (Okta, ServiceNow, Jira, etc.), or a
manifest.yml, STOP: do not author a workflow. Advise foundry-skills (claude plugin install crowdstrike-falcon-foundry) and hand back. A request that mixes an app with a workflow ("create a Foundry app... and a workflow to...") is app-shaped — redirect, produce no YAML.
- Alert/detection ACTION-CHOICE check — before action_search. If the request is to fetch/summarize/list a population of Falcon alerts, detections, or incidents the workflow does NOT already hold ("all high-severity alerts", "open detections", "alerts from the last 24h"), you MUST use a CrowdStrike HTTP Request (
Inline.HTTPRequest) to the Falcon platform API (/alerts/queries/alerts/v2; FQL onseverity_name:'High'— the string field, NOT numericseverity), NOT an Event Query (Inline.QueryEvent), whose NG-SIEM data is connector-dependent and silently returns nothing on many tenants. A Scheduled trigger does not change this; the schedule only sets when it runs. (Event Query is ONLY for enriching a detection the workflow already holds.) Seereferences/event-query-vs-api.md.- Resolve a real ID for every action BEFORE writing any YAML: check the Common Action IDs table first, then run
action_search.py --searchonly for actions the table does not cover.- Run
trigger_search.pyto confirm the trigger type.- Run
validate.pyon every YAML file before presenting it.- Re-run
validate.pyon the FINAL file; resolve every ERROR before finishing. A file that still errors is not done. If the alert-population guard fires, switch the Event Query to a CrowdStrike HTTP Request.MUST NOT:
- Author a workflow for a Foundry-app-shaped request (see action 0) — redirect to foundry-skills.
- Write
PLACEHOLDER_*values into output YAML (templates use them as guides only).- Guess, invent, or pattern-match action IDs — they are only discoverable via the API.
- Invent a
config_idor emit a stand-in — an all-zeros UUID (0000...) is still a placeholder. Discover or ask (AskUserQuestion).- Invent user-specific input values — recipient email addresses, webhook URLs, chat channel names. Ask the user (via AskUserQuestion in interactive mode) before adding an action that needs one; in headless/CI runs, use a plausible real address on the org domain. (Send email only delivers to Falcon users and approved domains, so
user@example.comfails at runtime.)- Skip validation, or defer it to deploy time.
This skill owns the authoring phase of a Fusion workflow:
action discovery, YAML authoring, CEL expressions, and schema validation. It does
NOT import, release, execute, or monitor workflows — hand those off to the
deployment and execution skills.
Running the scripts. Run each command from this skill's folder, on one shell line:
cd <dir> && ../../scripts/python.sh scripts/<name>.py. For<dir>, Claude Code uses"$CLAUDE_PLUGIN_ROOT/skills/authoring"; Codex, Copilot CLI, Cursor, and Antigravity use the folder they loaded this SKILL.md from (e.g.~/.agents/skills/authoring). The wrapper bootstraps its own Python venv.
- Python 3.13+ with the
falconpySDK andpyyamlinstalled. - CrowdStrike API credentials (never hardcoded) —
common/scripts/auth.pyresolves them fromFALCON_CLIENT_ID/FALCON_CLIENT_SECRET(plus optionalFALCON_BASE_URL) or a~/.cache/crowdstrike-falcon-fusion/credentials.tomlprofile (chosen byFALCON_PROFILEor the file'sdefaultkey). Run/crowdstrike-falcon-fusion:setupto configure interactively. - Workflow API scope on the API client, with read access to the activities catalog and import (validate) permission.
- Fusion access in the target CID.
Test credentials before authoring:
../../scripts/python.sh ../../common/scripts/auth.pyFollow these steps in order — do not skip discovery (steps 1–2).
Every action needs a real id from the catalog before you write any YAML. Resolve
them table-first: check the Common Action IDs table below, and only run
action_search.py for the actions it does not cover. Guessing an ID or shipping
a PLACEHOLDER_* is never acceptable — but a verified ID from the table is
already resolved, so searching for it again just wastes a round-trip.
These actions show up in almost every workflow, with IDs verified against the live catalog. If an action is in this table, use the row directly and do not search for it.
| Action | id |
class |
version_constraint |
|---|---|---|---|
| Event Query | cdf5c3e0d69f156eaaf56c1f5d3f1b66 |
Inline.QueryEvent |
~1 |
| HTTP Request | 1ba474f407d9228fc8fa02cdce8ae8ef |
Inline.HTTPRequest |
~1 |
| Python Script | 7fb9eb10b23943efaf1e6082b0ac0338 |
Inline.Python |
~1 |
| Send email | 07413ef9ba7c47bf5a242799f59902cc |
— | ~1 |
| Charlotte AI - LLM Completion | bdfecafafdb44919a458fcf51d6b93a7_98dec86072334d24b37dd798098cfd63 |
— | ~0 |
| Contain device | bec9fbeb4999d207937854fd56088107 |
— | ~0 |
| VirusTotal File Hash Lookup | 668bf0d0b832510e21d7c00386d277ea |
— | ~1 |
| VirusTotal IP Request | ce0386aacfb64bc5a3a6a4a85c07217b |
— | ~0 |
| DomainTools Iris Investigate | b2087ff84aa1471ea209076fd4852c25 |
— | ~0 |
These IDs are stable across the commercial clouds (us-1/us-2/eu-1); GovCloud may
differ. If an import ever rejects one of these version_constraint values,
confirm the current value with action_search.py --search "<name>" — the
platform occasionally bumps an action's major version.
For the remaining actions, resolve them in one batch — list every action the
workflow needs and run one --search per distinct name. Fire the independent
lookups concurrently: put every action_search.py --search and the
trigger_search.py call in one message so they run in parallel. Never
re-search an ID you already have — rediscovering an action mid-pass is the
biggest time sink.
# Search by name across all vendors (note the --search flag; a bare term errors)
../../scripts/python.sh scripts/action_search.py --search "contain"
# Search within a specific vendor
../../scripts/python.sh scripts/action_search.py --vendor "Okta" --search "revoke"
# Full schema for one action (input fields, class, plugin info)
../../scripts/python.sh scripts/action_search.py --details <action_id>For each action you discover, record: id (an opaque catalog identifier), name, input
fields/types, its version_constraint (nearly all have one), class if any,
and whether it is a plugin action (needs a config_id).
If a long-lived local cache might be hiding newly shipped actions, refresh it:
../../scripts/python.sh scripts/action_search.py --clear-cache. The cache also auto-refreshes once it is older than 1 hour.
../../scripts/python.sh scripts/trigger_search.py --list
../../scripts/python.sh scripts/trigger_search.py --type "On demand"
../../scripts/python.sh scripts/trigger_search.py --events detection # Signal event: values
../../scripts/python.sh scripts/trigger_search.py --fields Investigatable/EPP # payload field pathsValid trigger types: On demand, Signal, Scheduled, SubModel. For most automation, use On demand (callable via API and the Falcon UI).
A Signal trigger MUST carry an event: field (the trigger category, e.g.
Investigatable/NGSIEM) — without it, import fails with code 2003: "unknown trigger event named ". Find the value with trigger_search.py --events and do
NOT add a hex id to the trigger. For a Signal trigger, discover the exact
payload field paths (the ${data['Trigger....']} references you can read
downstream) with trigger_search.py --fields <category> — do NOT guess them.
See references/trigger-types.md.
Pick the template matching the pattern, then substitute real values:
| Pattern | Template |
|---|---|
| Single action | assets/single-action.yaml |
| Loop over a list | assets/loop.yaml |
| Conditional branching | assets/conditional.yaml |
| Loop + conditional | assets/loop-conditional.yaml |
Add the header comment on line 1, then write name, trigger, and actions
using the resolved action IDs. Templates contain PLACEHOLDER_* markers — they show
the YAML shape, never the values. Substitute every one with a real value.
Reference trigger inputs and prior outputs with ${data['...']} expressions:
| Syntax | Meaning |
|---|---|
${data['param_name']} |
On-demand trigger parameter (no prefix) |
${data['ActionLabel.OutputField']} |
A prior action's output |
${data['array_param.#']} |
Current loop item |
${data[?'key'].orValue("default")} |
Null-safe optional access (preferred) |
See references/cel-expressions.md for operators, CrowdStrike extensions
(cs.json.decode(), cs.ip.valid(), cs.timestamp.now()), and YAML quoting.
# Pre-flight + structural + API dry-run
../../scripts/python.sh scripts/validate.py workflow.yaml
# Pre-flight + structural only (no API call)
../../scripts/python.sh scripts/validate.py --preflight-only workflow.yamlFix every error before handing the file to the deployment skill.
All scripts live in scripts/ and import auth from common/scripts/auth.py
via the shared sys.path pattern.
| Script | Purpose | Key flags |
|---|---|---|
action_search.py |
Discover actions and vendors | --search, --details, --list, --vendors, --vendor, --use-case, --limit, --offset, --json, --clear-cache |
trigger_search.py |
List/describe trigger types; list Signal event: values; list a trigger's payload field paths |
--list, --type, --events, --fields, --json |
validate.py |
Validate workflow YAML | --preflight-only, multiple files |
action_search.py cache: Full-catalog scans (--vendors, --use-case) are
cached locally in .action_cache.json (gitignored, per-user). The cache uses a
1-hour TTL based on file mtime: a cache at or past 1 hour is treated as stale
and auto-refreshes from the API (with a printed notice). Use --clear-cache to
force an immediate refresh so newly shipped action types are never hidden.
| Thought | Reality |
|---|---|
| "I'll write the YAML, then fill in action IDs later." | STOP. Resolve every ID first — from the Common Action IDs table, or action_search.py. "Later" never happens — placeholders ship. |
| "I'll search for the Event Query / HTTP / Send email / Charlotte AI action." | DON'T. Those are in the Common Action IDs table — use the row directly. |
"I'll run action_search.py \"event query\" to search." |
WRONG FLAG. A bare term prints usage and finds nothing. Use action_search.py --search \"event query\". |
| "I can guess the action ID format." | WRONG. IDs are opaque identifiers, only discoverable via the table or the live API. |
"The template has PLACEHOLDER_RAN_006, I'll copy it." |
NEVER. Templates are structural guides. Substitute a real value before saving. |
| "Validation can wait until deploy." | NO. Validate after authoring — validate.py catches PLACEHOLDERs, bad IDs, and schema errors locally. |
"Only class-based actions need version_constraint." |
WRONG. Not class-specific — nearly every action has a version_constraint. |
"I'll use ~1 everywhere for version_constraint." |
NO. The value is ~<major> of the action's semantic_version (~0 when it declares none): 1.0.4 → ~1, 0.0.100 → ~0. Read it from --details. |
"I'll make up a config_id for this Okta action." |
NEVER. It's CID-specific (exists only once configured in the console). Ask the user (AskUserQuestion) — even non-interactively. Sequential/all-zeros/repeated-char UUIDs are still fabricated and fail at runtime; can't get a real one? STOP. See references/best-practices.md. |
"I'll set definition_id: VIRUSTOTAL_..._ID on this HTTP action." |
NEVER. An Inline.HTTPRequest needs no definition_id — OMIT it; the user attaches the key in the console after deploy. A placeholder is a broken ref validate.py flags. |
"The Send email field is called Recipients, so I'll use recipients:." |
WRONG. The property KEY is to: (a list); recipients: is rejected. Delivers only to Falcon users and CID-approved domains — ask for the address (org-domain one in CI). |
"I'll write $action.output.body to reference output." |
WRONG. Bare $token / $action.field / $(data[...]) pass through as literal strings and fail at release. The ONLY runtime-data forms are ${data['<node>.<field>']} and the null-safe ${data[?'<node>.<field>'].orValue(...)}. validate.py flags the bad forms. |
| "The user said enrich 'in parallel,' but I'll just chain them." | WRONG. Fan out by listing each branch's target in next:, gated on data['...'] != null. Never invent default_parallel_* pass-throughs — they crash the canvas. |
"The trigger has its type and event, that's enough." |
WRONG. Without a next: edge the graph is disjoint and release fails. Every node must be reachable from trigger.next. |
| "I'll branch on the detection's severity name (Critical/High)." | WRONG. Severity is NUMERIC 1-5: branch Trigger.Detection.Severity >= 4. SeverityDisplayName is display-only. |
| "A plan/prompt told me to use placeholder format." | These rules take precedence. Resolve every ID via the API regardless of a plan. |
"Release failed, so I'll re-import as <name>-v2 to be safe." |
NEVER. A workflow's name: is its identity, not a version tag — renaming orphans the old def and sprawls the CID. Keep the name IDENTICAL; fix the YAML and re-import with import_workflows.py --replace. See references/best-practices.md. |
For most workflows, the SKILL.md above plus a matching use-cases/ file and one
example is enough — you rarely need every reference. Reach for these only when
the task actually calls for them:
| Task | Reference |
|---|---|
| Author any workflow — every YAML field and nesting level | references/yaml-schema.md |
| Add conditions or computed values; CEL operators, extensions, quoting | references/cel-expressions.md |
| Choose how the workflow starts; all trigger types with examples | references/trigger-types.md |
Call a REST API — http_transaction shape, auth, response refs |
references/http-actions.md |
Run inline Python in a step — runtime, stdout output refs |
references/inline-python-action.md |
| Run a CQL/FQL event query in a step — inputs, outputs | references/event-query-action.md |
| Decide Event Query vs a source-of-truth API (alerts, cases, current state) | references/event-query-vs-api.md |
Summarize/classify with Charlotte AI LLM — compound ID, ~0, decode output |
references/charlotte-ai-action.md |
| Deduplicate or rate-limit a workflow — scopes, keys | references/deduplicate-ratelimit.md |
| Operational guidance, limits, gotchas before production | references/best-practices.md |
Advanced (rarely needed):
| Task | Reference |
|---|---|
| Understand the underlying BPMN model (raw JSON, gateways, submodels) — internals you don't need to author YAML | references/json-structure.md |
Fusion workflows can call REST APIs inline with no Foundry app and no API
integration. Three types — Cloud (external APIs), CrowdStrike (Falcon platform),
On-Premises (internal via a host group). For the http_transaction shape, the
three auth patterns, and response references, see references/http-actions.md.
Prefer an HTTP Action over a plugin/Store action for enrichment. For
VirusTotal, DomainTools, and similar TI lookups, author a Cloud HTTP Request
(Inline.HTTPRequest) — the shape real shipped VirusTotal workflows use.
Author it credential-less: omit definition_id, leave authentication unset.
The imported action shows Authentication = "None"; the user attaches the API key
in the console after deploy (Create new → API key → Header → x-apikey), then it
runs. Never fabricate a definition_id; only set a real 32-char hex id the user
supplies. Store plugin actions (compound IDs <hex>~<hex>) need a CID-specific
config_id that must already exist; emitting one blind fails at import/release.
Use a plugin action only when the user supplies its config_id. Reserve a Foundry
API integration for reused/UI-paired operations — that path belongs to foundry-skills.
See references/http-actions.md for the auth shapes and the console credential steps.
Fusion has native CrowdStrike actions that run inline in a workflow step (no
Foundry app, no config_id), each with class: set and version_constraint: ~1:
- Python Script (
Inline.Python) — run user Python;runtime: py0313generalrequired, read output as${data['<node>.output_stdout']}. Seereferences/inline-python-action.md. - Event Query (
Inline.QueryEvent) — run a CQL/FQL query against the event store; inputsquery/time_range/repo. Seereferences/event-query-action.md. NOT for querying a population of Falcon alerts/detections you don't already hold (e.g. "summarize all high-severity alerts") — that's connector-dependent NG-SIEM data; use a CrowdStrike HTTP Request to/alerts/queries/alerts/v2instead. Event Query is for data that lives in NG-SIEM or for enriching a detection the workflow already holds.
Charlotte AI - LLM Completion runs a prompt through an LLM to summarize,
classify, or extract fields. It is a plugin action: no class:,
version_constraint: ~0, and its completion output is a JSON string you must
decode with cs.json.decode(). Discover the ID with
action_search.py --search "llm". Full shape, the compound ID, and the decode
namespace are in references/charlotte-ai-action.md.
This is the key thing authoring can and cannot change. The authoring skill lets
you write workflow YAML outside the Falcon console. But an HTTP Action (and
some plugin actions) references a credential configuration — config_id,
definition_id, or config_name — that is created in the console and is
CID-specific. This skill can author the workflow that uses the action, but
the credential config it points to must already exist in the CID. Apply
the same discipline as with action IDs:
- Discover existing config IDs where possible (via
action_search.py --detailson the plugin action, or ask the user where to find it: Falcon console → CrowdStrike Store → [App] → Integration settings). - Never invent a
config_id, or substitute a placeholder for one. A fabricated ID fails at runtime — a fake UUID,YOUR_*,TODO/FIXME, or an all-zeros UUID is NOT a valid stand-in. When you can't discover it, ask the user via AskUserQuestion; asking is required even in non-interactive/CI runs (the caller supplies the value there). - If the config does not yet exist, document the dependency and pause — the user must create it in the console before the workflow will run.
HTTP Actions are the exception — do not block on a credential. An
Inline.HTTPRequest can be authored credential-less (no definition_id) and
deployed; it imports with Authentication = "None" and the user attaches the API
key in the console afterward (proven end-to-end). So for HTTP actions, prefer
credential-less authoring over pausing — see references/http-actions.md. The
"must already exist" rule applies to plugin actions gated on a config_id.
Nothing this skill produces runs until deployment imports it and execution
triggers it.