← Files CrowdStrike Falcon FusionARCHIVED FILE

skills/authoring/references/event-query-vs-api.md

7.52 KB · Oct 5, 2026 · 18:32 UTC

↓ Download file

# Event Query vs. a source-of-truth API

Two ways to get data in a workflow, and they answer different questions. Picking
the wrong one produces a workflow that looks right but returns incomplete or
empty results.

## The mental model

- **Event Query (`Inline.QueryEvent`) = ask the historian.** It searches
  NG-SIEM/LogScale for *what happened* — historical patterns, aggregations,
  trends, counts over time, debugging what occurred. It reads logs, not live
  object state.
- **A source-of-truth API = ask the system of record.** It answers *does X
  exist* and *what is the current state of X*, and it performs create / update /
  delete on a known object (cases, hosts, users, incidents, detections, alerts).

**Governing rule: if the object you care about has an API, call the API.** Event
Query is the escape hatch for data that has no dedicated object API (raw ingested
logs, custom parsers) or when the API you need does not exist yet.

## Which to use

| Use an Event Query when… | Use a source-of-truth API when… |
|--------------------------|----------------------------------|
| "What happened over time?" — patterns, trends | "Does X exist right now?" — existence check |
| Aggregations, counts by field, dedup across events | Get / read the current state of a known object |
| Enriching a detection you already hold (`Ngsiem.alert.id = ?detectID`) | Fetching the alert/detection *population* you don't have |
| Searching ingested logs with no object API | Create / update / delete a known object |
| Debugging why something did (or didn't) fire | Acting on whether an object exists |

## Reach for a source-of-truth API, in this order

When the question is about current state or existence, do NOT infer it from logs.
Prefer, in order:

1. **A native platform action**, if one exists — e.g. *CrowdStrike Cases → Search
   Cases* (`filter: detection_ids:'<id>'`), or the Hosts / Incidents actions.
   Discover these with `action_search.py`. No app, no code.
2. **A CrowdStrike HTTP Request** to the Falcon platform API when no native action
   covers it — e.g. `/alerts/queries/alerts/v2`, `/detects/queries/detects/v1`.
   Tenant-authenticated, standalone, no app. See `http-actions.md`.
3. **A Foundry function calling FalconPy** — same API, more ceremony, but the app
   is distributable/certifiable and prompts for credentials on install. Belongs to
   the `foundry-skills` plugin (e.g. `Alerts.query_alerts_v2`). Choose this when the
   workflow must be shared across CIDs.

## Why "infer from logs" goes wrong

A worked example: *"Has a case been created for this detection?"*

- **Log-inference (fragile):** query workflow execution logs for the "create case"
  workflow and join against detections. This answers a *different* question — "did
  this specific workflow run?" — and misses cases created manually, via the API, or
  by any other workflow. It is indirect, multi-repo, and brittle.
- **Source of truth (correct):** *CrowdStrike Cases → Search Cases* with
  `filter: detection_ids:'<detection_id>'`. Results > 0 means a case exists,
  regardless of how it was created.

## Falcon platform alerts, detections, and incidents

These are the most common trap. Their data in NG-SIEM/LogScale depends entirely on
the customer's **ingestion connectors** — a tenant with no connectors returns
audit-log events, not alerts, so an Event Query for "high-severity alerts" can
silently return zero. The split is about whether you already hold the object:

- **Enriching a detection you already hold** → depends on the detection type.
  When a workflow is triggered on a detection and has its ID, how you pull more
  fields depends on what kind of detection fired:
  - **First-party and third-party detections** → Event Query. Go schemaless
    (detection field shapes vary) and **match the detection's composite ID
    against `Ngsiem.alert.id`, NOT `Ngsiem.detection.id`.** The Signal trigger's
    `Trigger.Detection.DetectionID` is the *composite* ID (`cid:...:cid:id`), and
    in the NG-SIEM event store that value lives in `Ngsiem.alert.id`;
    `Ngsiem.detection.id` holds a different, short ID, so a query keyed on it
    silently returns **zero rows** (verified live). So:

    ```
    # RIGHT — composite DetectionID matches Ngsiem.alert.id
    Ngsiem.alert.id = ?detectID
    # WRONG — returns 0 rows for a composite DetectionID
    Ngsiem.detection.id = ?detectID
    ```

  - **Correlation-rule detections** → hydrate with the same
    `Ngsiem.alert.id = ?detectID` query. It works, but returns **multiple records**
    (the underlying events plus a correlation "meta-event" that only signals the
    rule fired), so `results[0]` is non-deterministic across runs. Drop the
    meta-event and keep the real events:

    ```
    Ngsiem.alert.id = ?detectID
    | xdr_type != correlation-rule-detection
    | report_name != *
    ```

    or project named columns with `table([field1, field2, ...])`. Restrict the
    action's output schema to only the fields you read, so runs that omit some
    fields don't fail schema validation. Event Query is how you reach the
    **event-level detail** (per-event source IP, country, and so on) that made up
    the detection. A **Get Detection Details** action returns the detection
    *object* instead — reach for it when the object's summary fields are all you
    need, or call `/alerts/entities/alerts/v2` with the composite `DetectionID` as
    `composite_id` for that same object.

  **Treat detection IDs as opaque.** Per the detections team, `composite_id` (what
  the trigger hands you) is the primary key used to retrieve a detection, but its
  tuple format may change — never split or parse it; match the whole string. If a
  hydration query returns nothing, confirm the join field by running the query in
  NG-SIEM → Advanced event search or via the NG-SIEM search API before assuming the
  detection has no data. **If the detection type carries indicators directly in the
  trigger payload (e.g. EPP: `Trigger.Detection.EPP.Process.SHA256`), prefer reading
  them straight from the payload over any hydration step — no join, no empty-result
  risk.** Discover payload fields with `trigger_search.py --fields <category>`.
- **Fetching the alert/detection *population* you don't have** — "summarize all
  high-severity alerts", "list open detections across products" → the Falcon
  platform API, NOT an Event Query (whether that population is in NG-SIEM is
  connector-dependent, so an Event Query can silently miss it). **Both options below
  call the same API; default to the first, mention the second for distribution:**
  - **Default — a CrowdStrike HTTP Request** to the API endpoint directly
    (`/alerts/queries/alerts/v2`, `/detects/queries/detects/v1`): tenant-authenticated,
    no Foundry app. Per CrowdStrike guidance an HTTP Action is the right tool for the
    vast majority of API integrations. FQL `severity_name:'High'+created_timestamp:>'now-24h'`
    (use `severity_name`, not the numeric `severity` field). Downside: the workflow
    isn't distributable — export/import only, no credentials travel with it.
  - **Foundry app + FalconPy `Alerts`/`Detects` function** (`query_alerts_v2`) — the
    same API via the FalconPy SDK; route to `foundry-skills`. More setup, but
    distributable/certifiable to other CIDs and prompts for credentials on install.
    Mention/suggest this only when the workflow needs to be shared or published.
- **Historical / aggregate analysis of alert telemetry that genuinely lives in
  NG-SIEM** (patterns over time, counts by vendor) → an Event Query is
  appropriate, subject to the same connector caveat.

SHA-256: 4abe7699d7116b16b1467b94d4a6f7a9c63dd543adc56f45383cf0b495075ccf