Docs
DocumentationQuery ReferenceAPI Reference
Open Console→→
DocumentationQuery ReferenceAPI Reference

Platform overview

What is Axiom?QuickstartArchitectureFeatures
Fundamentals
Datasets
Edge deployments
Limits
Performance
Optimize usage
Requirements
Semantic conventions
Glossary
Tour
SecurityRoadmap

Send data

Reference architecturesMethods

Understand data

Console
Query
Builder
Editor
Query results
Visualize
Traces
Metrics
Correlations
Save queries
Stream
Dashboard
Create
Elements
Create
Configure
Element types
Gauge
Heatmap
Log stream
Monitor list
Note
Pie chart
Scatter plot
Statistic
Table
Time series
Sections
Configure
Filter
Annotate
Monitor
Overview
View status
Configure
Examples
Monitor types
Anomaly
Match
Threshold
Alerting
Overview
Configure
Notifier types
Custom Webhook
Discord
Email
Microsoft Teams
Opsgenie
PagerDuty
Slack
Manage
Datasets
Overview
Views
Virtual fields
Access
RBAC
Tokens
CLI
Organization
Audit log
Settings
Usage and billing
Profile
Extend
Overview
AWS Lambda
AWS PrivateLink
Cloudflare Workers
Cloudflare Logpush
Convex
Grafana
Hex
Netlify
Supabase
Tailscale
Terraform
Unkey
Vercel
Intelligence
Overview
Spotlight
AI agents
Overview
MCP Server
Overview
Tools
Query cost limits
Agent-created orgs
Skills
Overview
Axiom alerting
Build dashboards
Control costs
Query metrics
SRE
Translate SPL to APL
Splunk
Overview
Splunk app
Install and configure
Commands
Examples
Portal
How it works
Set up standard mode
Set up transparent mode
Observability Cloud
OCSF data
OCSF as CIM
SPL command support
Examples
Monitor and troubleshoot

Use cases

ObservabilityProduct analytics
OCSF security data
Overview
Send data
Query data
LLM observability
Overview
Use Axiom AI SDK
Manual instrumentation
GenAI attributes
Redaction policies

Miscellaneous

LLMs
Overview
List of docs pages
Full docs
Query reference
FAQs
Legal
Acceptable use policy
Cookies
Data processing
HIPAA
Partner agreement
Partner program guide
Privacy policy
SLA
Terms of service
Terms of use
Use cases/OCSF security data

Query OCSF data

Learn how to query OCSF data in Axiom with APL, including typed columns, the unmapped map field, and arrays of objects such as observables and attacks.

This page shows how to query an OCSF dataset with APL. It covers the end of the OCSF flow in Axiom: the dataset is prepared and receives conformed events, as described in Send OCSF data to Axiom. To search the same data from Splunk, see Search OCSF data from Splunk.

Rendering diagram…

The examples run against the 100 synthetic events in the OCSF for Axiom pack’s axiom_normalize_post_processor sample after conforming. The outputs show what each query returns for that sample. Replace ocsf with the name of your dataset.

Choose the right reference

Every OCSF attribute lives in exactly one place. The place determines how you reference it in APL:

AttributeStored asAPL reference
class_uid, severity_idPromoted columnclass_uid
user.name, src_endpoint.ipPromoted column['user.name']
attacks, observablesMap field, array of objects['attacks'][0]['technique']['uid']
device.os.nameUnder unmapped['unmapped']['device']['os']['name']

Column names that contain dots must be quoted with brackets, for example ['src_endpoint.ip']. For more information, see Entity names. Inside map fields, use index notation for each level. For more information, see Map fields.

To see which columns your dataset actually contains, use getschema. If an attribute isn’t a column, it’s either inside a map field or under unmapped:

APL
['ocsf']
| getschema

Filter and group on numeric *_id columns rather than their string siblings. For example, use status_id == 2 rather than status == 'Failure'. Producers spell the strings differently, but the IDs are fixed by OCSF. The OCSF schema browser lists the meaning of every ID.

Count events by class

Start with an inventory of the classes in the dataset:

APL
['ocsf']
| summarize events = count() by class_uid, class_name
| sort by events desc
class_uidclass_nameevents
4001Network Activity40
4003DNS Activity15
3002Authentication15
1007Process Activity10
4002HTTP Activity10
2004Detection Finding5
6003API Activity5

Filter on promoted columns

Failed logons by user and source address. In the Authentication class (3002), activity_id 1 is a logon, status_id 2 is a failure, and user is the account that tried to log on:

APL
['ocsf']
| where class_uid == 3002 and activity_id == 1 and status_id == 2
| summarize failures = count() by ['user.name'], ['src_endpoint.ip'], status_detail
| sort by failures desc
user.namesrc_endpoint.ipstatus_detailfailures
heidi10.24.111.174Unknown user name or bad password.1
dave10.30.177.45Unknown user name or bad password.1
alice10.25.217.127Unknown user name or bad password.1

Promoted columns are typed, so numeric comparisons and sums need no conversion. Top destinations by bytes in Network Activity (4001):

APL
['ocsf']
| where class_uid == 4001
| summarize bytes = sum(['traffic.bytes']) by ['dst_endpoint.ip']
| top 3 by bytes
dst_endpoint.ipbytes
198.51.100.182441038
192.0.2.51405202
198.51.100.193403470

Read fields from unmapped

unmapped keeps the original OCSF nesting, so you read a field with one index per level. Values inside a map field are dynamic, so convert them with tostring, toint, or a similar function before you group or compare. The operating system of the device that recorded each logon is under unmapped because device.os is an object inside an object:

APL
['ocsf']
| where class_uid == 3002
| extend os = tostring(['unmapped']['device']['os']['name'])
| summarize events = count() by os
osevents
Windows Server 202215

To find which keys each class carries under unmapped, list the top-level keys with bag_keys and expand them into rows:

APL
['ocsf']
| where isnotnull(unmapped)
| extend keys = bag_keys(unmapped)
| mv-expand keys
| summarize events = count() by class_name, key = tostring(keys)
| sort by class_name asc, key asc
class_namekeyevents
API Activityactor5
API Activityapi5
API Activitycloud5
API Activitymetadata5
Authenticationdevice15
Authenticationmetadata15
Detection Findingevidences5
Detection Findingfinding_info5
Network Activitymetadata40
Process Activitydevice10

The Detection Finding events also show an array of objects under unmapped. Index into it like any other array:

APL
['ocsf']
| where class_uid == 2004
| project ['device.hostname'],
    rule = tostring(['unmapped']['finding_info']['analytic']['name']),
    process = tostring(['unmapped']['evidences'][0]['process']['name'])
device.hostnameruleprocess
wks-014.corp.example.comRare destinationrundll32.exe
srv-db-02.corp.example.comRare destinationrundll32.exe
………

Querying a map field uses more query-hours than querying a column. If you query an unmapped field often, define a virtual field for it so your team can reuse it by name.

Query arrays of objects

The six promoted arrays, observables, vulnerabilities, answers, attacks, file.hashes, and process.file.hashes, are each stored whole in their own map field. Every element keeps its own attributes, so an observable’s value stays next to its type and a technique stays next to its tactic.

To read one element, index it:

APL
['ocsf']
| where class_uid == 2004
| project ['finding_info.title'], ['device.hostname'],
    technique = tostring(['attacks'][0]['technique']['uid']),
    tactic = tostring(['attacks'][0]['tactic']['name'])
finding_info.titledevice.hostnametechniquetactic
Suspicious outbound connectionwks-014.corp.example.comT1071Command and Control
Suspicious outbound connectionsrv-db-02.corp.example.comT1071Command and Control
…………

To work with every element, expand the array into rows with mv-expand. For example, list every IP address observable in Detection Findings. In observables, type_id 2 is an IP address:

APL
['ocsf']
| where class_uid == 2004
| mv-expand observables
| where toint(observables['type_id']) == 2
| summarize findings = count() by ip = tostring(observables['value'])
ipfindings
192.0.2.1451
198.51.100.151
192.0.2.2211
192.0.2.1991
203.0.113.351

To pull one attribute from every element without expanding rows, use array_extract. It returns an array per event:

APL
['ocsf']
| where class_uid == 2004
| extend techniques = array_extract(['attacks'], @'$[*].technique.uid')
| project ['device.hostname'], techniques

Investigate an entity across classes

Because all classes share one dataset and one vocabulary, one query can follow an entity across event types. Everything the sample recorded about one workstation:

APL
['ocsf']
| where ['device.hostname'] == 'wks-014.corp.example.com'
    or ['src_endpoint.hostname'] == 'wks-014.corp.example.com'
    or ['dst_endpoint.hostname'] == 'wks-014.corp.example.com'
| summarize events = count() by class_name
class_nameevents
Detection Finding2
Process Activity2

Events are sparse: each class fills only its own columns. In cross-class queries, expect empty values in columns that a class doesn’t use, and guard with isnotnull or isnotempty where it matters.

What’s next

  • Search OCSF data from Splunk
  • Map fields
  • APL overview
Was this page helpful?
Suggest edits on GitHub
PreviousSend OCSF data to AxiomNextLLM observability
On this page
Choose the right referenceCount events by classFilter on promoted columnsRead fields from unmappedQuery arrays of objectsInvestigate an entity across classesWhat’s next