Spl To Apl
by axiomhq
Translates Splunk SPL queries to Axiom APL. Provides command mappings, function equivalents, and syntax transformations. Use when migrating from Splunk, converting SPL queries, or learning APL equivalents of SPL patterns.
Skill Details
Repository Files
4 files in this skill directory
name: spl-to-apl description: Translates Splunk SPL queries to Axiom APL. Provides command mappings, function equivalents, and syntax transformations. Use when migrating from Splunk, converting SPL queries, or learning APL equivalents of SPL patterns.
SPL to APL Translator
Expert translator from Splunk Processing Language (SPL) to Axiom Processing Language (APL).
Translation Principles
- Structure differs: SPL uses
index=... | command | command. APL uses['dataset'] | operator | operator - Time is explicit in APL: SPL time pickers don't translate — add
where _time between (...)based on the intended range - Field names: SPL uses bare
field, APL usesfieldor['field.with.dots'] - Pipe semantics are similar: Both flow data left-to-right through transformations
- Verify before deploying: Test translated queries; some SPL behaviors have no direct APL equivalent
Command Mappings
Core Commands
| SPL | APL | Notes |
|---|---|---|
search index=... |
['dataset'] |
Dataset replaces index |
search field=value |
where field == "value" |
Explicit where clause |
search *error* |
search "error" or where msg has "error" |
has is faster |
where |
where |
Same semantics |
stats |
summarize |
Different syntax for aggregations |
eval |
extend |
Create/modify fields |
table |
project |
Select columns |
fields |
project or project-keep |
Select columns |
fields - |
project-away |
Remove columns |
rename |
project-rename new = old |
Single operator |
sort |
order by or sort by |
Similar |
head N |
take N or limit N |
Get first N rows |
tail N |
take N after order by ... asc |
Reverse sort, then take |
top N field |
summarize count() by field | top N by count_ |
Two-step |
rare N field |
summarize count() by field | top N by count_ asc |
Ascending order |
dedup field |
summarize arg_max(_time, *) by field |
Keep latest row per field value |
rex |
parse or extract() |
Regex extraction |
regex |
where ... matches regex |
Filter by regex |
join |
join |
Preview feature - limited to 50k rows, inner/innerunique/leftouter only |
append |
union |
Combine datasets |
mvexpand |
mv-expand |
Expand arrays |
spath |
Access via ['field']['path'] or parse_json() |
JSON access |
lookup |
lookup |
Similar |
fillnull |
extend field = coalesce(field, default) |
Use coalesce |
transaction |
No direct equivalent | Use summarize with make_list() |
bucket |
bin() |
Time bucketing |
timechart |
summarize ... by bin(_time, span) |
Manual binning |
chart |
summarize ... by field1, field2 |
Group by multiple |
eventstats |
join kind=leftouter (subquery) on keys |
Compute aggregates in subquery, then join |
streamstats |
No direct equivalent | Use summarize with binning for approximations |
Stats → Summarize Translation
Basic Pattern
# SPL
| stats count by status
# APL
| summarize count() by status
Aggregation Functions
| SPL | APL | Notes |
|---|---|---|
count |
count() |
Parentheses required |
count(field) |
countif(isnotnull(field)) |
Count non-null |
dc(field) |
dcount(field) |
Distinct count |
sum(field) |
sum(field) |
Same |
avg(field) |
avg(field) |
Same |
min(field) |
min(field) |
Same |
max(field) |
max(field) |
Same |
median(field) |
percentile(field, 50) |
Use percentile |
stdev(field) |
stdev(field) |
Same |
var(field) |
variance(field) |
Different name |
range(field) |
max(field) - min(field) |
Calculate manually |
first(field) |
arg_min(_time, field) |
First value by time |
last(field) |
arg_max(_time, field) |
Last by time |
list(field) |
make_list(field) |
Collect to array |
values(field) |
make_set(field) |
Unique values as array |
perc95(field) |
percentile(field, 95) |
Percentile syntax |
p95(field) |
percentile(field, 95) |
Same |
percentile(field, 50, 95, 99) |
percentiles_array(field, 50, 95, 99) |
Multiple percentiles |
earliest(field) |
min(field) or arg_min(_time, *) |
Earliest value/row |
latest(field) |
max(field) or arg_max(_time, *) |
Latest value/row |
earliest_time |
min(_time) |
Earliest timestamp |
latest_time |
max(_time) |
Latest timestamp |
Complex Stats Examples
# SPL: Multiple aggregations
| stats count, avg(duration), max(bytes) by host
# APL
| summarize count(), avg(duration), max(bytes) by host
# SPL: Conditional count
| stats count(eval(status>=500)) as errors by host
# APL
| summarize errors = countif(status >= 500) by host
# SPL: Named aggregations
| stats count as total, dc(user) as unique_users by app
# APL
| summarize total = count(), unique_users = dcount(user) by app
Eval → Extend Translation
Basic Pattern
# SPL
| eval new_field = old_field * 2
# APL
| extend new_field = old_field * 2
Function Mappings
| SPL | APL | Notes |
|---|---|---|
if(cond, true, false) |
iff(cond, true, false) |
Double 'f' |
case(c1,v1, c2,v2, ...) |
case(c1, v1, c2, v2, ..., default) |
Requires default |
coalesce(a, b, c) |
coalesce(a, b, c) |
Same |
null() |
dynamic(null) or typed: string(null) |
Typed null literals |
isnull(x) |
isnull(x) |
Same |
isnotnull(x) |
isnotnull(x) |
Same |
len(str) |
strlen(str) |
Different name |
lower(str) |
tolower(str) |
Different name |
upper(str) |
toupper(str) |
Different name |
substr(str, start, len) |
substring(str, start, len) |
0-indexed in APL |
replace(str, old, new) |
replace_string(str, old, new) |
Different name |
split(str, delim) |
split(str, delim) |
Same |
mvjoin(mv, delim) |
strcat_array(arr, delim) |
Join array |
mvcount(mv) |
array_length(arr) |
Array length |
mvindex(mv, idx) |
arr[idx] |
Array indexing |
strftime(time, fmt) |
Use datetime_part() + strcat() |
No direct equivalent (build manually) |
strptime(str, fmt) |
todatetime(str) |
Parse datetime |
now() |
now() |
Same |
relative_time(time, mod) |
Use datetime_add() |
Manual calculation |
tonumber(str) |
toint(str) / tolong(str) / toreal(str) |
Explicit types |
tostring(val) |
tostring(val) |
Same |
urldecode(str) |
url_decode(str) |
Different name |
match(str, regex) |
str matches regex "pattern" |
Operator not function |
cidrmatch(cidr, ip) |
ipv4_is_in_range(ip, cidr) |
Args reversed |
abs(x) |
abs(x) |
Same |
ceil(x) / ceiling(x) |
ceiling(x) |
Different name |
floor(x) |
floor(x) |
Same |
round(x, n) |
round(x, n) |
Same |
log(x, base) |
log(x) / log(base) or log10(x) |
Manual base |
pow(x, y) |
pow(x, y) |
Same |
sqrt(x) |
sqrt(x) |
Same |
random() |
rand() |
Different name |
md5(str) |
hash_md5(str) |
Different name |
sha1(str) |
hash_sha1(str) |
Different name |
sha256(str) |
hash_sha256(str) |
Different name |
Conditional Logic Examples
# SPL: if statement
| eval severity = if(status >= 500, "error", "ok")
# APL
| extend severity = iff(status >= 500, "error", "ok")
# SPL: case statement
| eval level = case(
status >= 500, "error",
status >= 400, "warning",
1==1, "ok"
)
# APL
| extend level = case(
status >= 500, "error",
status >= 400, "warning",
"ok"
)
Time Handling
Time Range Translation
# SPL (time picker: Last 24 hours)
index=logs
# APL (explicit time range required)
['logs'] | where _time between (ago(24h) .. now())
Timechart Translation
# SPL
| timechart span=5m count by status
# APL
| summarize count() by bin(_time, 5m), status
# SPL: Timechart with aggregation
| timechart span=1h avg(duration) as avg_duration
# APL
| summarize avg_duration = avg(duration) by bin(_time, 1h)
Time Functions
| SPL | APL |
|---|---|
now() |
now() |
relative_time(now(), "-1d@d") |
startofday(ago(1d)) |
strftime(_time, "%Y-%m-%d") |
Use tostring() or datetime_part() |
earliest=-1h |
where _time >= ago(1h) |
latest=now |
where _time <= now() |
_time |
_time |
Time Literals
| SPL | APL |
|---|---|
-1h / earliest=-1h |
ago(1h) |
-7d |
ago(7d) |
@d (start of day) |
startofday(now()) |
@w (start of week) |
startofweek(now()) |
Rex → Parse/Extract Translation
Named Group Extraction
# SPL
| rex field=message "user=(?<username>\w+)"
# APL - using parse with regex
| parse kind=regex message with @"user=(?P<username>\w+)"
# APL - using extract function
| extend username = extract("user=(\\w+)", 1, message)
Pattern Extraction (Non-Regex)
# SPL
| rex field=uri "^/api/(?<version>v\d+)/(?<endpoint>\w+)"
# APL
| parse uri with "/api/" version "/" endpoint
Multiple Captures
# SPL
| rex field=log "src=(?<src_ip>\d+\.\d+\.\d+\.\d+).*dst=(?<dst_ip>\d+\.\d+\.\d+\.\d+)"
# APL
| extend src_ip = extract(@"src=(\d+\.\d+\.\d+\.\d+)", 1, log)
| extend dst_ip = extract(@"dst=(\d+\.\d+\.\d+\.\d+)", 1, log)
String Matching
| SPL | APL | Performance |
|---|---|---|
search "error" |
search "error" |
Slow (all fields) |
field="value" |
where field == "value" |
Fastest |
field="*value*" |
where field contains "value" |
Moderate |
field="value*" |
where field startswith "value" |
Fast |
field="*value" |
where field endswith "value" |
Fast |
match(field, "regex") |
where field matches regex "pattern" |
Slowest |
like(field, "val%") |
where field startswith "val" |
Fast |
Performance Tips
- Use
hasovercontains— word-boundary matching is faster - Use
_csvariants — case-sensitive is faster (has_cs,contains_cs) - Avoid regex when simple operators work
- Filter by time FIRST — always
where _time between ...early
Common Query Translations
Error Count by Host
# SPL
index=logs status>=500
| stats count by host
# APL
['logs']
| where _time between (ago(1h) .. now())
| where status >= 500
| summarize count() by host
Top 10 URIs
# SPL
index=logs
| top limit=10 uri
# APL
['logs']
| where _time between (ago(1h) .. now())
| summarize count() by uri
| top 10 by count_
Percentile Latency Over Time
# SPL
index=logs
| timechart span=5m perc95(duration) as p95
# APL
['logs']
| where _time between (ago(1h) .. now())
| summarize p95 = percentile(duration, 95) by bin(_time, 5m)
Error Rate Calculation
# SPL
index=logs
| stats count(eval(status>=500)) as errors, count as total by host
| eval error_rate = errors/total*100
# APL
['logs']
| where _time between (ago(1h) .. now())
| summarize errors = countif(status >= 500), total = count() by host
| extend error_rate = toreal(errors) / total * 100
Dedup by User (Keep Latest)
# SPL
index=logs
| sort - _time
| dedup user
# APL
['logs']
| where _time between (ago(1h) .. now())
| summarize arg_max(_time, *) by user
Transaction-like Grouping
# SPL
index=logs
| transaction session_id maxspan=30m
# APL
['logs']
| where _time between (ago(1h) .. now())
| summarize
start_time = min(_time),
end_time = max(_time),
events = make_list(pack("time", _time, "action", action)),
duration = max(_time) - min(_time)
by session_id
Join Datasets
# SPL
index=logs
| join user_id [search index=users | fields user_id, name, email]
# APL
['logs']
| where _time between (ago(1h) .. now())
| join kind=inner (['users']) on user_id
Subsearch / Subquery
# SPL
index=logs [search index=errors | fields user_id | format]
# APL
let error_users = ['errors'] | where _time between (ago(1h) .. now()) | distinct user_id;
['logs']
| where _time between (ago(1h) .. now())
| where user_id in (error_users)
Field Escaping
SPL vs APL Field Names
# SPL - dots in field names
kubernetes.pod.name="frontend"
# APL - bracket notation for dots (in where clause)
| where ['kubernetes.pod.name'] == "frontend"
Special Characters
Fields with dots that are part of the field name (not hierarchy) need escaping:
// Field name literally contains "app.kubernetes.io"
| where ['kubernetes.labels.app\\.kubernetes\\.io/name'] == "frontend"
// Use getschema to discover exact field names
['dataset'] | where _time between (ago(1h) .. now()) | getschema
Reference
See reference/command-mapping.md for complete command list.
See reference/function-mapping.md for complete function list.
For APL documentation: https://axiom.co/docs/apl/introduction For detailed operator docs: Use the axiom-sre skill references.
Related Skills
Attack Tree Construction
Build comprehensive attack trees to visualize threat paths. Use when mapping attack scenarios, identifying defense gaps, or communicating security risks to stakeholders.
Grafana Dashboards
Create and manage production Grafana dashboards for real-time visualization of system and application metrics. Use when building monitoring dashboards, visualizing metrics, or creating operational observability interfaces.
Matplotlib
Foundational plotting library. Create line plots, scatter, bar, histograms, heatmaps, 3D, subplots, export PNG/PDF/SVG, for scientific visualization and publication figures.
Scientific Visualization
Create publication figures with matplotlib/seaborn/plotly. Multi-panel layouts, error bars, significance markers, colorblind-safe, export PDF/EPS/TIFF, for journal-ready scientific plots.
Seaborn
Statistical visualization. Scatter, box, violin, heatmaps, pair plots, regression, correlation matrices, KDE, faceted plots, for exploratory analysis and publication figures.
Shap
Model interpretability and explainability using SHAP (SHapley Additive exPlanations). Use this skill when explaining machine learning model predictions, computing feature importance, generating SHAP plots (waterfall, beeswarm, bar, scatter, force, heatmap), debugging models, analyzing model bias or fairness, comparing models, or implementing explainable AI. Works with tree-based models (XGBoost, LightGBM, Random Forest), deep learning (TensorFlow, PyTorch), linear models, and any black-box model
Pydeseq2
Differential gene expression analysis (Python DESeq2). Identify DE genes from bulk RNA-seq counts, Wald tests, FDR correction, volcano/MA plots, for RNA-seq analysis.
Query Writing
For writing and executing SQL queries - from simple single-table queries to complex multi-table JOINs and aggregations
Pydeseq2
Differential gene expression analysis (Python DESeq2). Identify DE genes from bulk RNA-seq counts, Wald tests, FDR correction, volcano/MA plots, for RNA-seq analysis.
Scientific Visualization
Meta-skill for publication-ready figures. Use when creating journal submission figures requiring multi-panel layouts, significance annotations, error bars, colorblind-safe palettes, and specific journal formatting (Nature, Science, Cell). Orchestrates matplotlib/seaborn/plotly with publication styles. For quick exploration use seaborn or plotly directly.
