sentinel-one/ai-siem/powerquery
Claude Code and agent skill by sentinel-one/ai-siem
- community
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/powerquerySKILL.md
SentinelOne PowerQuery
<!-- CONFIG-FILE-ADDRESSING v1 -->
SDL config files: address by `udoId`, and do not trust a REST listing.RESTlistFiles/getFilecannot see udoId-addressed/dashboards/files, sogetFilereturns404on a dashboard the console is displaying and the listing under-reports (measured onone tenant: REST 8, GraphQL 17, console 48 files). **If a listing disagrees with what the UIshows, the listing is wrong until proven otherwise**, change read path before concluding theobject is missing or the token lacks scope. Use the GraphQLconfigFiles/configFilesurface.A name-addressedaddConfigFileto/dashboards/creates a duplicate instead of updating;address dashboards byudoIdwithexpectedVersion.contentis HJSON, not JSON.S1-Scopechanges which files exist as far as the caller can tell.Full detail: `sdl-api/references/config-file-graphql.md`
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_runruns any PQ through the LRQ API (plusqueryType: "LOG"for raw events,slices/mergefor long windows,outputFilefor bulk results),powerquery_enumerate_sourceslists data sources, andpowerquery_schema_discoversamples a source's fields. The server runs on the user's machine, so it reaches*.sentinelone.netwhere 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.mdin the s1-secops-skills source repo,mcp/s1-secops-mcp/README.mdin 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:
- 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.
- Draft the query following the grammar below. Favor
filter | group | sort | limit | columnsas the default shape; it's what most real investigations need. - Run it against the tenant with
powerquery_run, which uses the Long Running Query (LRQ) API atPOST /sdl/v2/api/querieson the tenant's console URL. LRQ is the fastest, highest-limit, most reliable path for any programmatic use and supersedes both/api/powerQueryand the Deep Visibility/dv/events/pqendpoint (both deprecated; sunset Feb 15 2027). It is async and parallelizes across time slices (slicesplusmerge). See "Running queries (LRQ API by default)" below andreferences/lrq-api.mdfor 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. - 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
slicesand amergespec (see "Long windows" under Running queries below). - 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
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 onesgroup agg(x), name2=agg2(y) by f1, "grouped name"=f2: aggregate; also creates a new record setsort +f1, -f2:-= descendinglimit N: truncate (default shows 10 without it; output is capped at 1,000 rows if nolimit/group)parse "…$field$…" from srcField: extract fields from unstructured textlookup 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. Seereferences/datasource-command.mdsavelookup '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 justjoin)| union (q1), (q2), …: merge heterogeneous result sets (up to 10 queries; use whenfilter (…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 onecompareallowed)| top K agg(x) by f1, f2: probabilistic top-N (fast on huge ranges;count()/sum()are "(estimated)",min/maxexact)| 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:
| Operator | Meaning | |
|---|---|---|
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 write | Write 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 aggregates | count(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 asc | sort -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 asp95(x).| limitwith no number returns 10 rows.- A query with no
columnsorgroupreturns onlytimestampandmessage. To see every field of an event, usequeryType: "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 / 0returns the stringInfinitywith 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
- SKILL.md in the repositoryCheckingplugins/s1-secops-skills/skills/powerquery/SKILL.md
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
[](https://verified-skill.com/skills/sentinel-one/ai-siem/powerquery)