Build custom reports with EQL
Use EQL reporting to build custom reports beyond the fixed templates: describe what you want in plain language, get a validated EQL query back, preview the results, then save the report (as JSON or PDF) or run it on a schedule.
Before you begin
Section titled “Before you begin”- EQL needs the tenant’s
eql-reportingfeature and a dedicated EQL reader on the deployment. When it’s off, calls return403. - The caller must be a tenant admin with universal API scope: issue the token with
"all_scopes": true. See Automate personal access tokens. - Queries are validated and run with bounded limits. Requests are rate limited (the spec gives one active EQL request per tenant and ten attempts per tenant per minute), and a
429means a budget was exceeded. Nothing is retried automatically.
What EQL queries look like
Section titled “What EQL queries look like”The spec doesn’t publish an EQL grammar. The query language is documented in the Elastio console; over the API, a query is a string of up to 8,192 bytes sent in a query field. The practical way to get a valid query is the draft endpoint below, which turns a plain-language request into EQL and explains it. The examples on this page pass the drafted query through rather than hand-writing one.
Discover datasets and capabilities
Section titled “Discover datasets and capabilities”The catalog lists the datasets you can query (with their columns), which optional features are available (aiAvailable for drafting, pdfAvailable, schedulesAvailable) and the limits that apply.
curl "https://acme.app.elastio.com/gateway/reporting/v1/eql/datasets" \ -H "Authorization: Bearer $ELASTIO_TOKEN"import os, requests
BASE = "https://acme.app.elastio.com"HEADERS = {"Authorization": f"Bearer {os.environ['ELASTIO_TOKEN']}"}
catalog = requests.get(f"{BASE}/gateway/reporting/v1/eql/datasets", headers=HEADERS, timeout=30).json()print("AI drafts:", catalog["aiAvailable"], "PDF:", catalog["pdfAvailable"], "Schedules:", catalog["schedulesAvailable"])for ds in catalog["datasets"]: print(ds["name"], "-", ds["title"], ds["columns"])$Base = "https://acme.app.elastio.com"$Headers = @{ Authorization = "Bearer $env:ELASTIO_TOKEN" }$Catalog = Invoke-RestMethod -Uri "$Base/gateway/reporting/v1/eql/datasets" -Headers $Headers$Catalog.datasets | Format-Table name, title, platform, shape$Catalog.limits{ "version": "…", "aiAvailable": true, "pdfAvailable": true, "schedulesAvailable": true, "datasets": [ { "name": "…", "title": "…", "columns": ["…", "…"], "shape": "array", "platform": "…" } ], "limits": { "maxRows": …, "maxQueryBytes": …, "maxResponseBytes": …, "maxPeriodDays": … }}Draft a query from plain language
Section titled “Draft a query from plain language”Send a prompt (up to 4,000 bytes) and a past UTC period of at most 90 days. The service sends your request and the public catalog definitions to Azure OpenAI (not customer rows) and returns either compiled EQL (status: "ready") or a question to answer first (status: "clarification"). Drafting never runs, saves or schedules anything.
curl -X POST "https://acme.app.elastio.com/gateway/reporting/v1/eql/draft" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Assets with active ransomware findings, grouped by AWS account", "periodStart": "2026-09-12T00:00:00Z", "periodEnd": "2026-10-12T00:00:00Z" }'period = {"periodStart": "2026-09-12T00:00:00Z", "periodEnd": "2026-10-12T00:00:00Z"}resp = requests.post(f"{BASE}/gateway/reporting/v1/eql/draft", headers=HEADERS, timeout=60, json={"prompt": "Assets with active ransomware findings, grouped by AWS account", **period})resp.raise_for_status()draft = resp.json()if draft["status"] == "clarification": print("Need more detail:", draft["clarification"])else: query = draft["query"] print(draft["explanation"])$Period = @{ periodStart = "2026-09-12T00:00:00Z"; periodEnd = "2026-10-12T00:00:00Z" }$Body = @{ prompt = "Assets with active ransomware findings, grouped by AWS account" } + $Period | ConvertTo-Json$Draft = Invoke-RestMethod -Method Post -Uri "$Base/gateway/reporting/v1/eql/draft" -Headers $Headers ` -ContentType "application/json" -Body $Bodyif ($Draft.status -eq "ready") { $Query = $Draft.query; $Draft.explanation } else { $Draft.clarification }{ "status": "ready", "query": "…", "explanation": "…", "clarification": ""}Other responses to handle: 422 (the generated EQL didn’t pass validation; rephrase), 502 (the AI provider was unavailable or returned invalid output) and 503 (AI isn’t configured; aiAvailable is false).
Preview, then save the report
Section titled “Preview, then save the report”Preview runs the query for a period and returns the rows inline without saving anything. Check truncated: if it’s true, the result hit the row or size limit.
# $QUERY holds the "query" string from the draft responsejq -n --arg q "$QUERY" '{query:$q, periodStart:"2026-09-12T00:00:00Z", periodEnd:"2026-10-12T00:00:00Z"}' |curl -X POST "https://acme.app.elastio.com/gateway/reporting/v1/eql/query" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ --data-binary @-preview = requests.post(f"{BASE}/gateway/reporting/v1/eql/query", headers=HEADERS, timeout=120, json={"query": query, **period})preview.raise_for_status()result = preview.json()print(result["columns"], result["rowCount"], "rows", "(truncated)" if result["truncated"] else "")$Body = @{ query = $Query } + $Period | ConvertTo-Json$Result = Invoke-RestMethod -Method Post -Uri "$Base/gateway/reporting/v1/eql/query" -Headers $Headers ` -ContentType "application/json" -Body $Body"$($Result.rowCount) rows, truncated: $($Result.truncated)"{ "version": "…", "columns": ["…", "…"], "data": [ … ], "rowCount": 42, "truncated": false}When the preview looks right, save it. Saving takes the same body plus optional format (json or pdf) and description (up to 500 characters), and returns 202 while the report is generated. Poll GET /gateway/reporting/v1/eql/reports/{id} until status is ready or failed, then download the PDF if you asked for one.
ID=$(jq -n --arg q "$QUERY" '{query:$q, periodStart:"2026-09-12T00:00:00Z", periodEnd:"2026-10-12T00:00:00Z", format:"pdf", description:"Ransomware exposure by account"}' | curl -s -X POST "https://acme.app.elastio.com/gateway/reporting/v1/eql/reports" \ -H "Authorization: Bearer $ELASTIO_TOKEN" -H "Content-Type: application/json" --data-binary @- | jq -r .id)
until [ "$(curl -s "https://acme.app.elastio.com/gateway/reporting/v1/eql/reports/$ID" \ -H "Authorization: Bearer $ELASTIO_TOKEN" | jq -r .status)" != "processing" ]; do sleep 10; done
curl "https://acme.app.elastio.com/gateway/reporting/v1/eql/reports/$ID/pdf" \ -H "Authorization: Bearer $ELASTIO_TOKEN" -o ransomware-exposure.pdfimport time
saved = requests.post(f"{BASE}/gateway/reporting/v1/eql/reports", headers=HEADERS, timeout=60, json={"query": query, **period, "format": "pdf", "description": "Ransomware exposure by account"})saved.raise_for_status()report_id = saved.json()["id"]
while True: report = requests.get(f"{BASE}/gateway/reporting/v1/eql/reports/{report_id}", headers=HEADERS, timeout=30).json() if report["status"] != "processing": break time.sleep(10)
if report["status"] == "failed": raise RuntimeError(report.get("error"))
pdf = requests.get(f"{BASE}/gateway/reporting/v1/eql/reports/{report_id}/pdf", headers=HEADERS, timeout=120)pdf.raise_for_status()open("ransomware-exposure.pdf", "wb").write(pdf.content)$Body = @{ query = $Query; format = "pdf"; description = "Ransomware exposure by account" } + $Period | ConvertTo-Json$Saved = Invoke-RestMethod -Method Post -Uri "$Base/gateway/reporting/v1/eql/reports" -Headers $Headers ` -ContentType "application/json" -Body $Bodydo { Start-Sleep -Seconds 10 $Report = Invoke-RestMethod -Uri "$Base/gateway/reporting/v1/eql/reports/$($Saved.id)" -Headers $Headers} while ($Report.status -eq "processing")Invoke-WebRequest -Uri "$Base/gateway/reporting/v1/eql/reports/$($Saved.id)/pdf" -Headers $Headers -OutFile "ransomware-exposure.pdf"The saved report includes the query it ran:
{ "id": "f0e1d2c3-b4a5-4968-8776-655443322110", "title": "…", "status": "ready", "query": "…", "format": "pdf", "periodStart": "2026-09-12T00:00:00Z", "periodEnd": "2026-10-12T00:00:00Z", "createdAt": "2026-10-12T09:20:00Z", "columns": ["…", "…"], "shape": "array", "rowCount": 42, "data": [ … ]}Downloading the PDF before the report is ready returns 409. GET /gateway/reporting/v1/eql/reports?page=N lists saved reports with items, total, page and pageSize.
Schedule an EQL report
Section titled “Schedule an EQL report”When schedulesAvailable is true, you can run a query on a cadence. name, template (always eql) and query are required. period, cadence and cadence_config work as for report schedules: period is last_24_hours, last_7_days, last_30_days or last_90_days; cadence is hourly, daily, weekly or monthly, with cadence_config shaped to match. format is json or pdf. The query is stored and revalidated each time the schedule fires. Results are saved as EQL reports only; email and webhook delivery aren’t supported.
jq -n --arg q "$QUERY" '{ name: "Monthly ransomware exposure", template: "eql", query: $q, period: "last_30_days", cadence: "monthly", cadence_config: { day_of_month: 1, time_utc: "06:00" }, format: "pdf"}' | curl -X POST "https://acme.app.elastio.com/gateway/reporting/v1/eql/schedules" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ --data-binary @-body = { "name": "Monthly ransomware exposure", "template": "eql", "query": query, "period": "last_30_days", "cadence": "monthly", "cadence_config": {"day_of_month": 1, "time_utc": "06:00"}, "format": "pdf",}resp = requests.post(f"{BASE}/gateway/reporting/v1/eql/schedules", headers=HEADERS, json=body, timeout=30)resp.raise_for_status()schedule = resp.json()$Body = @{ name = "Monthly ransomware exposure" template = "eql" query = $Query period = "last_30_days" cadence = "monthly" cadence_config = @{ day_of_month = 1; time_utc = "06:00" } format = "pdf"} | ConvertTo-Json -Depth 3$Schedule = Invoke-RestMethod -Method Post -Uri "$Base/gateway/reporting/v1/eql/schedules" -Headers $Headers ` -ContentType "application/json" -Body $Body{ "id": "a9b8c7d6-e5f4-4321-8a0b-1c2d3e4f5a6b", "name": "Monthly ransomware exposure", "template": "eql", "period": "last_30_days", "cadence": "monthly", "cadence_config": { "day_of_month": 1, "time_utc": "06:00" }, "format": "pdf", "enabled": true, "created_at": "2026-10-12T09:30:00Z", "created_by": "jane.doe@example.com", "last_run_at": null, "next_run_at": "2026-11-01T06:00:00Z", "sync_status": "pending", "query": "…"}To pause a schedule, send PATCH /gateway/reporting/v1/eql/schedules/{id} with {"enabled": false}. The same call can change the query or cadence.
All endpoints
Section titled “All endpoints”EQL
12 operations in the reference →- GET
/gateway/reporting/v1/eql/datasetsDiscover EQL datasets and capabilities - POST
/gateway/reporting/v1/eql/draftDraft EQL from a natural-language report request - POST
/gateway/reporting/v1/eql/queryPreview a bounded EQL report - GET
/gateway/reporting/v1/eql/reportsList saved EQL reports - POST
/gateway/reporting/v1/eql/reportsGenerate and save an EQL report - GET
/gateway/reporting/v1/eql/reports/{id}Get a saved EQL report and its query - GET
/gateway/reporting/v1/eql/reports/{id}/pdfDownload a completed EQL PDF - GET
/gateway/reporting/v1/eql/schedulesList EQL report schedules - POST
/gateway/reporting/v1/eql/schedulesSchedule an EQL report - GET
/gateway/reporting/v1/eql/schedules/{id}Get an EQL report schedule - PATCH
/gateway/reporting/v1/eql/schedules/{id}Update or pause an EQL schedule - DELETE
/gateway/reporting/v1/eql/schedules/{id}Delete an EQL report schedule