e9-eql
Skill1 starsChanged 48 days agoApache-2.0View on GitHub
Write and run engine9 Query Language (EQL) — expression fragments and SELECT query objects — primarily via the MCP `eql` tool (and `sql` compile_eql). Use when the user mentions EQL, buildSqlFromEQLObject, query objects with table/columns/conditions, asks how to query account data through MCP, or wants a SQL table analysis (min, max, distinct, samples) via MCP `analyze`.
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 30
Prefer eql over raw SQL when the user is describing a structured query (table + columns + filters).
- Very specific: line 56
Do not retry with sql or task as a workaround.
- Very specific: line 63
When you need a fragment (not a full SELECT), use sql:
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
engine9 EQL
EQL is engine9’s SQL-like expression language plus a JSON query object that
compiles to a SELECT. Use this skill to build structured account queries,
compile expression fragments, or choose between EQL and adjacent data tools.
The primary way to submit an EQL query object from Cursor is the engine9 MCP
eql tool.
Quick reference
| Intent | Tool |
| Run a SELECT from an EQL query object | eql |
| Convert one EQL expression to a SQL fragment | sql with command: "compile_eql" |
| Run hand-written SQL | sql with command: "query" (or sql set) |
| Analyze a table (min, max, distinct, samples, date buckets) | analyze |
| Date histogram only | sql with command: "histo" |
| Schema only | sql describe / indexes / tables |
Prefer eql over raw SQL when the user is describing a structured query (table + columns + filters). Prefer analyze when they want mins, maxes, distinct counts, and samples.
Workflow
- Log in —
mcp_auth→ user completes prompt →ok/user(e9-mcp Step 0). - Know
account_id— from/e9a, this chat’s session scope, oraccountsearch. - Optional discovery —
sqltables/describe, or MCPanalyze, to learn table shape before writing EQL. See Analyzing a table. - Call
eql:
{
"account_id": "<account_id>",
"eql": {
"table": "transaction",
"columns": ["*"],
"conditions": [{ "eql": "amount > 10" }],
"orderBy": [{ "column": "ts", "orderByDirection": "desc" }],
"limit": 5
}
}- Read the response —
{ ok: true, sql, data, columns, max_rows, row_count, truncated }. The generatedsqlis useful for debugging;data/columnsare the rows. MCP hard-caps at 10000 rows.
Rule: On MCP errors (isError, unauthorized, or database unreachable), stop.
Do not retry with sql or task as a workaround.
Rule: Do not guess account scope or read leftover CLI files such as
.e9_parameters; ask or suggest /e9a instead.
Expression-only: compile_eql
When you need a fragment (not a full SELECT), use sql:
{
"account_id": "<account_id>",
"command": "compile_eql",
"table": "person",
"eql": "YEAR(modified_at)"
}Returns a cleaned SQL fragment and refsByTable. Do not use the eql tool for fragments alone.
File format
Query object shape
buildSqlFromEQLObject / MCP eql accept a non-array object:
| Field | Required | Notes |
table | yes | Base table name, or alias when subquery is set |
columns | yes* | Array of column specs. Alias: fields. At least ["*"] |
conditions | no | AND’d WHERE clauses |
groupBy | no | Group expressions / columns |
orderBy | no | Columns or { column, orderByDirection } (asc/desc) |
limit / offset | no | Pagination. MCP eql defaults limit to 10000 and will not exceed that |
joins | no | { table, join_eql, alias?, type? } — type: inner (default), left, right, outer |
subquery | no | Nested EQL object; outer table is the alias for the subquery |
* Legacy callers may use fields instead of columns.
having is not part of this MCP query-object builder (segment search may use it elsewhere). Express filters with conditions / groupBy + column EQL, or use raw sql.
Column specifications
Any of:
"id"
"person.email as email"
{ "column": "id" }
{ "column": "ts", "name": "event_ts" }
{ "eql": "YEAR(modified_at)", "name": "year_modified" }
{ "eql": "count(id)", "name": "count" }Rules:
- Plain strings without
(are treated as column names (optionaltable.column). - Strings with
(are parsed as EQL expressions. { eql: "..." }requiresnameas an alias.*/{ "column": "*" }selects all columns from the table.
Rule: Every { eql: "..." } column specification must include name.
Condition specifications
AND’d together. Prefer EQL strings for clarity:
{ "eql": "amount > 10" }
{ "eql": "id in (1,2,3)" }
{ "eql": "email like '%@example.com'" }Typed conditions (UI-style):
{
"type": "EQUALS",
"values": [
{ "ref": { "column": "id" } },
{ "value": { "value": "3" } }
]
}Supported type values: EQUALS, NOT_EQUALS, LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, LIKE, NOT_LIKE, CONTAINS, DOES_NOT_CONTAIN, IS_NULL, IS_NOT_NULL.
Joins
{
"table": "person",
"joins": [
{
"table": "person_email",
"join_eql": "person.id=person_email.person_id"
},
{
"table": "person_email",
"alias": "work_emails",
"type": "left",
"join_eql": "person.id=work_emails.person_id and work_emails.email_type='Work'"
}
],
"columns": [
{ "eql": "person.id", "name": "id" },
{ "eql": "person_email.email", "name": "email" },
{ "eql": "work_emails.email", "name": "work_email" }
],
"limit": 25
}Each join needs table + join_eql. Optional alias (defaults to table name) and type.
Concepts
Expression language
EQL expressions appear inside columns[].eql, conditions[].eql, groupBy, and join_eql. Syntax is SQL-like; the server dialectizes functions.
Comparisons and logic
x < y
x = 1
x and y and not z
x between 1 and 2
1 in (x, y, 4, 5)
1 not in (x, y)
"test" like "t%"
x is null
x is not nullMath and casts
sum(total) / count(*)
(test1.x + test2.y) * 2
cast(x as char)
cast(x as signed)Conditionals
if(x, y, z)
ifnull(x, y)
ifnull(x, y, z)
case when x = 1 then "test" else "hello" end
case x when 1 then "one" when 2 then "two" else "other" endif(...) compiles to case when .... ifnull with multiple args chains non-null checks.
Dates
now()
getdate()
date_add(now(), interval 1 day)
date_sub(now(), interval 1 day)
now() + interval 1 day
now() - interval 1 day
YEAR(modified_at)Aggregates and distinct
count(*)
count(id)
sum(amount)
distinct xBucket example (amount bands)
case when amount <= 20 then '$0-$20'
when amount > 20 and amount <= 50 then '$20-$50'
when amount > 50 and amount <= 100 then '$50-$100'
when amount > 100 then '$100+' endMore expression round-trips live in examples.md.
Examples
Simple select
{
"table": "person",
"columns": ["id"],
"limit": 1
}Filter + order + limit (MCP integration style)
{
"table": "transaction",
"columns": ["*"],
"conditions": [{ "eql": "amount > 10" }],
"orderBy": [{ "column": "ts", "orderByDirection": "desc" }],
"limit": 5
}Aggregates + groupBy
{
"table": "person",
"columns": [
{ "eql": "YEAR(modified_at)", "name": "year_modified" },
{ "eql": "count(id)", "name": "count" }
],
"conditions": [
{ "eql": "YEAR(modified_at) > 2020" },
{ "eql": "id in (1,2,3)" }
],
"groupBy": [{ "eql": "YEAR(modified_at)" }]
}Subquery
Outer table is the alias for the nested SELECT:
{
"table": "subquery_1",
"subquery": {
"table": "person_email",
"columns": [
{ "eql": "person_id", "name": "person_id" },
{ "eql": "count(*)", "name": "emails" }
],
"groupBy": [{ "eql": "person_id" }]
},
"columns": [
{
"eql": "case when subquery_1.emails > 1 then 'multi-email' else 'one-email' end",
"name": "level"
},
{ "eql": "count(person_id)", "name": "people" }
],
"groupBy": [
{
"eql": "case when subquery_1.emails > 1 then 'multi-email' else 'one-email' end"
}
]
}Analyzing a table
MCP analyze analyzes one or more SQL tables: inferred types, min, max, empty counts, distinct counts, frequent samples, and (when an indexed datetime column exists) a full-table date histogram. Use it when the question is about the shape of a table. Schema-only questions stay on sql describe / indexes / tables. A histogram without the per-column analysis is sql command: "histo" (any datetime column; index not required).
Rule: Call analyze for a table analysis. Do not hand-write MIN, MAX, or COUNT(DISTINCT) to rediscover the same stats.
Call
{
"account_id": "<account_id>",
"table": "transaction",
"max_tables": 1,
"target_buckets": 10
}| Argument | Required | Notes |
account_id | yes | Account whose warehouse to analyze |
table | yes | Exact name or filter. tables({ filter }) tries a regex first, then language tokens ("ROI transaction") |
max_tables | no | How many matches to analyze. Default 3, max 10 |
target_buckets | no | Approximate date-histogram bucket count when an indexed datetime column exists. Default 10 |
Response: { ok, filter, matched_tables, analyzed_tables, truncated, analyses }. Each analyses[] entry is { table, records, table_records, columns }. table_records is count(*). Column stats live on columns. Histogram buckets are merged onto those same column objects.
Column stats
| Field | Meaning |
name | Column name |
type | Inferred from sampled values: string, int, bigint, double, decimal, date, datetime, uuid |
min / max | Smallest and largest values in the sample. When a histogram runs, every datetime column is replaced with full-table SQL min/max (see below) |
empty | Sampled rows where the value was null or missing |
distinct | Distinct values seen in the sample |
sample | Up to 32 most frequent values in the sample, stringified |
min_length / max_length | Present on string columns |
buckets | On datetime columns when a histogram runs: { range, start, end, records, min, max } |
bucket_column | true on the column the histogram is cut on |
bucket_unit | day, week, month, quarter, or year when buckets exist |
The histogram column is chosen in this order: ts, created_at, modified_at, otherwise the first indexed datetime column that leads an index. Every datetime column receives the same buckets, with that column’s own min / max inside each bucket. bucket_column is true only on the column used to cut the ranges. analyze only auto-runs the histogram when an indexed datetime leading column exists; for unindexed dates use sql command: "histo" with an explicit column.
Sample vs full table
Rule: table_records is count(*) for the whole table.
Rule: min, max, distinct, empty, sample, and records come from a row sample of up to 10000 rows: 5000 from the start of the table plus 5000 highest primary-key rows when the table has a primary key, or 10000 rows when it does not. They are not full-table aggregates. A row that appears in both reads can be counted twice.
Rule: When the table has an indexed datetime column, analyze also runs a SQL date histogram. That histogram’s MIN / MAX replace min and max on every datetime column. buckets[].records are full-table counts for those ranges. records on the analysis itself stays the sample size. table_records stays count(*).
target_buckets only changes the histogram. It does not change the row sample.
Rules
Rule: Stop and report MCP tool errors; do not invent SQL workarounds.
Rule: Analyze a table with MCP analyze. Sample min/max are not full-table aggregates; indexed datetime min/max from the histogram are.
- Authenticated via MCP;
account_idknown - Prefer MCP
eqlfor query objects;sqlcompile_eqlfor fragments columnsis a non-empty array (use["*"]if needed)- Every
{ eql: "..." }column has aname - Joins use objects with
table+join_eql(not bare strings) - Discover table/column names via
sql/analyze, not local schema guesses when MCP is connected - Table analyses (min, max, distinct, samples, date buckets) use MCP
analyze, includingtarget_bucketswhen the histogram grain matters - On tool error, stop and report
Related documentation
| Topic | Documentation |
| Additional expression round-trips | Examples |
| MCP login, account scope, and tool-error handling | e9-mcp |
/e9 and /e9a command-style interactions | e9-cli |
In this repository24 other files it gives its agents.
- .cursor/rules/no-client-names.mdcCursor rule
- .cursor/rules/skill-writing-standard.mdcCursor rule
- create-engine9-pluginSkill
- e9-api-keySkill
- e9-cliSkill
- e9-custom-fieldsSkill
- e9-delegate-domainSkill
- e9-dev-tasksSkill
- e9-exportSkill
- e9-funnelSkill
- e9-global-messageSkill
- e9-inputSkill
- e9-inventorySkill
- e9-mcpSkill
- e9-modelSkill
- e9-person-idSkill
- e9-person-remoteSkill
- e9-pii-freeSkill
- e9-pluginSkill
- e9-reportsSkill
- e9-source-codeSkill
- e9-tasks-apiSkill
- e9-timelineSkill
- e9-upgradeSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

