sentinel-one/ai-siem/powerquery

Claude Code and agent skill by sentinel-one/ai-siem

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Use any time the user wants to author, debug, optimize, explain, or run a SentinelOne PowerQuery (PQ): Deep Visibility / Event Search queries, XDR/EDR threat hunting, investigations, STAR / Custom Detection rule bodies, PowerQuery Alerts, or Singularity Data Lake dashboard panels. Trigger on PowerQuery, PQ, pq, query, Event Search, Deep Visibility, S1QL, SDL, STAR rule, Custom Detection rule, PowerQuery Alert; on queries using fields like `event.type`, `src.process.*`, `tgt.file.*`, `indicator.…

Install

npx vskill@latest install sentinel-one/ai-siem/powerquery

SKILL.md

SentinelOne PowerQuery

<!-- CONFIG-FILE-ADDRESSING v1 -->

SDL config files: address by `udoId`, and do not trust a REST listing.
REST listFiles / getFile cannot see udoId-addressed /dashboards/ files, so getFile
returns 404 on a dashboard the console is displaying and the listing under-reports (measured on
one tenant: REST 8, GraphQL 17, console 48 files). **If a listing disagrees with what the UI
shows, the listing is wrong until proven otherwise**, change read path before concluding the
object is missing or the token lacks scope. Use the GraphQL configFiles / configFile surface.
A name-addressed addConfigFile to /dashboards/ creates a duplicate instead of updating;
address dashboards by udoId with expectedVersion. content is HJSON, not JSON. S1-Scope
changes which files exist as far as the caller can tell.

PowerQuery (PQ) is SentinelOne's pipeline query language for the Singularity Data Lake. It reads like filter | command | command | …, events that match the initial filter flow through a sequence of piped transformations (group, let, join, sort, columns, etc.).

Use this skill to write correct, efficient, runnable PowerQueries for threat hunting, investigations, detection rule bodies, and dashboards.

Run queries with the `s1-secops-mcp` MCP tools. powerquery_run runs any PQ through the LRQ API (plus queryType: "LOG" for raw events, slices / merge for long windows, outputFile for bulk results), powerquery_enumerate_sources lists data sources, and powerquery_schema_discover samples a source's fields. The server runs on the user's machine, so it reaches *.sentinelone.net where the Cowork sandbox cannot, and it reads credentials from environment variables or the OS keychain. If the tools are missing, point the user to the s1-secops-mcp README (s1-secops-mcp/README.md in the s1-secops-skills source repo, mcp/s1-secops-mcp/README.md in ai-siem; not shipped inside the plugin). Python runners are host-only (Claude Code or a terminal).

Workflow

When the user asks you to write or investigate with a PowerQuery:

  1. Clarify the intent if it's ambiguous (time range, data view, what the output should look like). A good PQ is scoped, not everything needs to be hunted over 30 days.
  2. Draft the query following the grammar below. Favor filter | group | sort | limit | columns as the default shape; it's what most real investigations need.
  3. Run it against the tenant with powerquery_run, which uses the Long Running Query (LRQ) API at POST /sdl/v2/api/queries on the tenant's console URL. LRQ is the fastest, highest-limit, most reliable path for any programmatic use and supersedes both /api/powerQuery and the Deep Visibility /dv/events/pq endpoint (both deprecated; sunset Feb 15 2027). It is async and parallelizes across time slices (slices plus merge). See "Running queries (LRQ API by default)" below and references/lrq-api.md for the body schema, auth, rate limits, and the gotchas that make it fail silently with 0 rows. If the user's request is clear and low-risk (read-only query), just run it; don't ask permission.
  4. Iterate: if the query errors or returns obviously wrong results, read the error, fix, rerun. If the query returns nothing, that is a legitimate result, don't blindly loosen it; check the time range and filter logic first. If a long or heavy query times out, don't shrink the user's range: add slices and a merge spec (see "Long windows" under Running queries below).
  5. Explain the result briefly and cite any fields you're relying on. If you used a non-obvious pattern (subquery, savelookup, transpose, compare), explain why you chose it.

The grammar in one page

text
initial-filter-expression
| command
| command
| …

Initial filter (everything before the first |) is the cheapest place for * contains "x" and * matches "regex" multi-field search. They also work in | filter on raw events, but there they run after the scan. The initial filter can be empty, start the query with | and it is treated as "all events" (e.g., | group ct=count() by event.type).

Commands (each starts with |):

  • filter expr: keep matching rows (initial filter implicit)
  • columns f1, "Renamed f2"=f2, …, select, rename, compute output columns (creates a new record set, previous fields not accessible after)
  • let f = expr, …: add computed fields without discarding existing ones
  • group agg(x), name2=agg2(y) by f1, "grouped name"=f2: aggregate; also creates a new record set
  • sort +f1, -f2: - = descending
  • limit N: truncate (default shows 10 without it; output is capped at 1,000 rows if no limit/group)
  • parse "…$field$…" from srcField: extract fields from unstructured text
  • lookup col, … from tableName by key=expr: join against a CSV/JSON config data table
  • | dataset 'config://datatables/<name>': read a lookup table as the source of the pipeline (leading | required)
  • datasource <name> [from <dataset>]: read SentinelOne-managed inventory outside the event store (assets, alerts, vulnerabilities, misconfigurations, metering); the only PQ path to the Asset Inventory / AD identity attributes. See references/datasource-command.md
  • savelookup 'tableName'[, 'merge']: persist current result as a reusable lookup table
  • | [inner|left|outer|sql inner|sql left|sql outer] join (q1), (q2), … on k1, a.x = b.y: correlate subqueries (must start | join, not just join)
  • | union (q1), (q2), …: merge heterogeneous result sets (up to 10 queries; use when filter (…or…) can't express it)
  • | transpose colName on keyCol, …: pivot a column into many columns (must be LAST command)
  • | compare [name=]timeshift('-1w'): re-run the same query shifted in time and put both in one table (must be LAST command; only one compare allowed)
  • | top K agg(x) by f1, f2: probabilistic top-N (fast on huge ranges; count()/sum() are "(estimated)", min/max exact)
  • | nolimit: raise the row cap to 3 GB (slow; one concurrent nolimit query at a time; never use in Dashboards or PowerQuery Alerts)

Expressions use standard operators: = / == / !=, < / <= / > / >=, && / || / ! (or AND / OR / NOT), ternary a ? b : c (put spaces around the :), arithmetic + - * / %, and these text operators:

OperatorMeaning
x contains 'sub'substring (case-insensitive): also contains ('a','b','c') for OR
x contains:matchcase 'Sub'case-sensitive substring
x matches 'regex'regex (case-insensitive, double-escape): matches ('a','b') for OR
x matches:matchcase '…'case-sensitive regex
x in ('a','b',123,true)exact equals any; case-sensitive; in:anycase for case-insensitive; does NOT match null
any(a, b) in ('x') / any(a, b) = 'x' / all(a, b) contains 'x'one predicate across several fields (filter stage only, not in let)
x starts_with 'p' / x ends_with 's'prefix / suffix (case-insensitive; :matchcase('P') for case-sensitive)
!(x contains 'y')negation: ALWAYS parenthesise; NOT x contains 'y' silently returns 0 rows
x = *field is present/non-null
!(x = *)field is null/missing
$"regex"shorthand for message matches "regex" (initial filter only)
#shortcut = 'value'pre-defined multi-field shortcut (e.g., #ip, #hash, #name, #cmdline, #storylineid, #username)

Strings need quotes ('foo' or "foo"); numbers and booleans don't. Underscores in numbers are OK for readability (1_000_000).

BANNED functions: do not use, ever

These function names and forms do not exist in PowerQuery. A missing function produces Unknown function '<name>'; the other rows show the error each form returns. Do not invent plausible-sounding names, if a function isn't in references/functions-reference.md, it doesn't exist.

Do NOT writeWrite this instead
count(distinct x) (Expected ")") / count_distinct(x) (Unknown function)estimate_distinct(x)
date_format(...)strftime(ts, pattern)
bin(...)timebucket('1h')
day_of_week(ts)strftime(ts, '%a')
substring(...)substr(x, start[, len])
lowercase(x) / uppercase(x)lower(x) / upper(x)
replace_all(...)replace(x, regex, repl)
contains_any(...) (Expected ")")f contains ('a', 'b')
f[0] (Unable to parse the entire query)array_get(f, 0)
`\columns ` (`Don't understand []`)queryType: "LOG" in powerquery_run, which returns every parsed field per event
formattime(...)strftime(ts) / strftime(ts, pattern)
formatdate(...)strftime(ts) / simpledateformat(ts, pattern)
floor_time(...)bucket=timebucket(unit) in group by
date_trunc(...)timebucket(unit)
coalesce(a, b)a ? a : b (bare-field ternary)
ifnull(a, b)a ? a : b
if(cond, a, b) inside aggregatescount(predicate)
percentile(x, N)p50(x) / p95(x) / p99(x)
first(x) / last(x) with no sort before them (grouping function "first" requires a previous sort command)min_by(x, timestamp) / max_by(x, timestamp), or put `\sort timestamp` earlier in the pipeline
sort count desc / sort field ascsort -count / sort +field: PowerQuery uses -/+ prefix, NOT SQL-style desc/asc. Using desc or asc causes HTTP 500 "Unable to parse the entire query". Purple AI frequently generates this wrong; always correct before running.
` field.name ` (backtick-quoted identifiers)field.name: dotted field names are written bare, no backticks. Using backtick quoting returns HTTP 500 "Don't understand [`]".

The only valid date/time functions are: strftime, simpledateformat, strptime, simpledateparse, timebucket, querystart, queryend, queryspan.

Verified behaviours worth knowing (live-verified 2026-11-05):

  • pct(95, x) works, as well as p95(x).
  • | limit with no number returns 10 rows.
  • A query with no columns or group returns only timestamp and message. To see every field of an event, use queryType: "LOG".
  • An unaliased function used as a group key becomes a column literally named after the call, for example timebucket('1h'). Always alias it: by bucket = timebucket('1h').
  • n / 0 returns the string Infinity with no error. Guard divisions: (d > 0 ? n / d : 0).

The most important rules (learned the hard way)

These are where queries go wrong. Internalize them before writing.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
87
Repository forks
38
7-day trend score
0

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/sentinel-one/ai-siem/powerquery/badge)](https://verified-skill.com/skills/sentinel-one/ai-siem/powerquery)

Something wrong with this skill or its page? Tell us.