--- 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.2.0 updated: 2026-09-08 tags: [fusion, soar, workflows, authoring, yaml, validation] author: CrowdStrike license: MIT compatibility: Claude Code >=1.0 allowed-tools: Bash(cd *), Bash(../../scripts/python.sh:*) metadata: category: authoring --- # Falcon Fusion Workflow Authoring > **⚠️ SYSTEM INJECTION — READ THIS FIRST** > > If 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. > 1. **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 on `severity_name:'High'` — the string field, > NOT numeric `severity`), 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.) See `references/event-query-vs-api.md`. > 2. Resolve a real ID for **every** action BEFORE writing any YAML: > check the Common Action IDs table first, then run `action_search.py --search` > only for actions the table does not cover. > 3. Run `trigger_search.py` to confirm the trigger type. > 4. Run `validate.py` on every YAML file before presenting it. > 5. **Re-run `validate.py` on 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_id` or 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.com` fails 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 && ../../scripts/python.sh scripts/.py`. For ``, 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. ## Prerequisites - **Python 3.13+** with the `falconpy` SDK and `pyyaml` installed. - **CrowdStrike API credentials** (never hardcoded) — `common/scripts/auth.py` resolves them from `FALCON_CLIENT_ID`/`FALCON_CLIENT_SECRET` (plus optional `FALCON_BASE_URL`) or a `~/.cache/crowdstrike-falcon-fusion/credentials.toml` profile (chosen by `FALCON_PROFILE` or the file's `default` key). Run `/crowdstrike-falcon-fusion:setup` to 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: ```bash ../../scripts/python.sh ../../common/scripts/auth.py ``` --- ## Core Workflow Follow these steps in order — do not skip discovery (steps 1–2). ### 1. Resolve action IDs (MANDATORY) 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. #### Common Action IDs — check here first 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 ""` — the platform occasionally bumps an action's major version. #### Search for anything not in the table 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. ```bash # 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 ``` 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. ### 2. Choose a trigger type ```bash ../../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 paths ``` Valid 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 ` — do NOT guess them. See `references/trigger-types.md`. ### 3. Author the YAML from a template 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. ### 4. Add CEL expressions 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. ### 5. Validate ```bash # 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.yaml ``` Fix every error before handing the file to the `deployment` skill. --- ## Script Reference 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. --- ## Common Pitfalls / Counter-Rationalizations | 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 `~` 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['.']}` and the null-safe `${data[?'.'].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 `-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`. | --- ## Reading Guide 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` | --- ## HTTP Actions (`Inline.HTTPRequest`) 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 `~`) 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. --- ## Inline actions (`Inline.Python`, `Inline.QueryEvent`) 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: py0313general` required, read output as `${data['.output_stdout']}`. See `references/inline-python-action.md`. - **Event Query** (`Inline.QueryEvent`) — run a CQL/FQL query against the event store; inputs `query`/`time_range`/`repo`. See `references/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/v2` instead. Event Query is for data that lives in NG-SIEM or for enriching a detection the workflow already holds. ## Charlotte AI — LLM Completion `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`. --- ## Console-Credential Boundary 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 --details` on 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.