Antithesis logomark
DOCS

Event sets

The event search API endpoint gives you a programmatic way to query the events Antithesis recorded during a test run. You describe the events you want using an “event set”, send it to the API, and get back the matching events as NDJSON.

Event sets here are conceptually the same as in Querying with event sets, where you use them interactively. This is a reference for writing event set queries for the event search API.

What’s an event set?

An event set is a set of the events recorded during a test run. You describe the set you want by writing a pipeline of operations, separated by . and applied left to right.

Each operation takes an event set as input, and produces a new event set. When you chain operations with ., the output of one becomes the input of the next. The first operation in a pipeline has nothing to its left, so its input is the set of all events recorded during the test run. Every pipeline begins from that complete set and you keep narrowing, reshaping, or combining them until you’re left with the events you care about.

An operation interacts with the event set in the following ways:

  • It selects which events stay in the set, e.g., keeping only events from one container.
  • It reshapes each event by adding or removing fields, e.g., annotating an event with information computed from other events that preceded it.

Because operations can add fields, an event partway through a pipeline may carry fields that weren’t in the event as originally recorded. Later operations see those added fields and can act on them. The Event schema describes an event as recorded by Antithesis, before any operation has reshaped it. The reshaped events are called synthetic events.

Let’s look at a simple example of an event set before looking at all available operations for creating an event set.

Example

Imagine your system under test has several containers in your system, and you just want two events from a specific container. Here’s the request body of the query that gets these events:

{
"query": "matches({container: \"fdb-workload-1\"})",
"limit": 2
}

Inside the request body the event set is a JSON string, so its inner double quotes are escaped (”). For readability, the examples below show each event set on its own, unescaped, so remember to escape the inner quotes when you place one in a query field.

The event set here is matches({container: "fdb-workload-1"}). It selects events only from fdb-workload-1, and limit caps the result to just two events.

The API responds with NDJSON of Event objects:

{"moment":{"input_hash":"4536913169440939055","vtime":"750.6794897380751"},"AtomicMutations":"0 -1 0","BatchPriorityReadVersions":"0 -1 0","BatchPriorityReadVersionsCompleted":"0 -1 0","BlobGranuleLocationRequests":"0 -1 0","BlobGranuleLocationRequestsCompleted":"0 -1 0","BytesRead":"0 -1 0","ClearMutations":"0 -1 0","ClientDescription":"external-7.3.75-2083768936062727749","Cluster":"docker","CommitCompleted":"0 -1 0","CommitStarted":"0 -1 3","CommitVersionNotFoundForSS":"0 -1 0","CommittedMutationBytes":"0 -1 0","CommittedMutations":"0 -1 0","DateTime":"2026-07-02T01:53:41Z","DefaultPriorityReadVersions":"0 -1 3","DefaultPriorityReadVersionsCompleted":"0 -1 0","Elapsed":"6.56977","ExpensiveClearCostEstCount":"0 -1 0","FutureVersions":"0 -1 0","GetAddressesForKeyRequests":"0 -1 0","GetKeyRequests":"0 -1 0","GetMappedRangeRequests":"0 -1 0","GetRangeRequests":"0 -1 0","GetRangeStreamRequests":"0 -1 0","GetValueRequests":"0 -1 0","ID":"088520088d239923","IPT_bytes_out":1023814952,"ImmediatePriorityReadVersions":"0 -1 0","ImmediatePriorityReadVersionsCompleted":"0 -1 0","Internal":"0","KeyServerLocationRequests":"0 -1 0","KeyServerLocationRequestsCompleted":"0 -1 0","KeysRead":"0 -1 0","Latency90":"0","Latency98":"0","LocationCacheEntryCount":"1","LogGroup":"default","LogicalUncachedReads":"0 -1 0","Machine":"10.89.0.6:80","MaxBytesPerCommit":"-1.79769e+308","MaxCommitLatency":"-1.79769e+308","MaxGRVLatency":"-1.79769e+308","MaxLatency":"-1.79769e+308","MaxMutationsPerCommit":"-1.79769e+308","MaxRowReadLatency":"-1.79769e+308","MaybeCommitted":"0 -1 0","MeanBytesPerCommit":"0","MeanCommitLatency":"0","MeanGRVLatency":"0","MeanLatency":"0","MeanMutationsPerCommit":"0","MeanRowReadLatency":"0","MedianBytesPerCommit":"0","MedianCommitLatency":"0","MedianGRVLatency":"0","MedianLatency":"0","MedianMutationsPerCommit":"0","MedianRowReadLatency":"0","MetadataVersionReads":"0 -1 0","NotCommitted":"0 -1 0","NumGrvFullBatches":"0 -1 0","NumGrvTimedOutBatches":"0 -1 3","NumLocalityCacheEntries":"1","OutstandingWatches":"0","PhysicalReadRequests":"0 -1 0","PhysicalReadRequestsCompleted":"0 -1 0","ProcessBehind":"0 -1 0","ReadVersionBatches":"0 -1 3","ReadVersions":"0 -1 3","ReadVersionsCompleted":"0 -1 0","ReadVersionsThrottled":"0 -1 0","ResourceConstrained":"0 -1 0","SetMutations":"0 -1 3","Severity":"10","StatusRequests":"0 -1 8","TenantLookupRequests":"0 -1 0","TenantLookupRequestsCompleted":"0 -1 0","ThreadID":"13587819877422497627","Throttled":"0 -1 0","Time":"1782957221.228375","TooOld":"0 -1 0","Type":"TransactionMetrics","WatchMapSize":"0","WatchRequests":"0 -1 0","source":{"container":"fdb-workload-1","name":"antithesis/pods/fdb-workload-1/trace.10.89.0.6.80.1782956528.pdw76G.0.2.json","pid":85}}
{"moment":{"input_hash":"6572773276055372305","vtime":"503.44814017694443"},"ClientDescription":"external-7.1.67-1285091599831140879","DateTime":"2026-07-02T01:49:51Z","ID":"0000000000000000","IPT_bytes_out":182368408,"LogGroup":"default","Machine":"10.89.0.6:139","PeerAddr":"10.89.0.9:4500(fromHostname)","Severity":"10","SuppressedEventCount":"6","ThreadID":"1285091599831140879","Time":"1782956991.856739","Type":"ConnectionTimedOut","source":{"container":"fdb-workload-1","name":"antithesis/pods/fdb-workload-1/trace.10.89.0.6.139.1782956560.xcUgXo.0.1.json","pid":195}}

Key points about the response:

  • Each event carries a moment field that identifies when the event happened within the run. Specifically, vtime identifies the time of event.
  • At most limit events are returned.

Refresher: the Antithesis multiverse

Antithesis exploration generates a multiverse of execution paths that the system under test took during the test run. This multiverse looks like a tree, a single starting root point that branches into timelines as exploration continues.

Sometimes when you’re triaging a hard-to-find bug or just understanding the flow of events that led to a specific observation, you’ll want to create complex event sets which, for instance, compute aggregations of events in a specific timeline, or run temporal queries. Visualizing the multiverse is beneficial when writing such event sets.

Here’s an example illustration of a multiverse map:

Multiverse map example. A subset of events are highlighted.

In one of the timelines highlighted in coral, event A is a part of event B’s execution history. This will come in handy when understanding temporal operations and aggregators like fold.

Event schema

An event represents anything that happened within the simulation.

Event emitters

The following will emit events:

  • stdout/stderr: Standard output of processes inside your containers will emit events.
  • SDK events: Our SDK assertions communicate with the rest of our system using events. You may also generate custom events using the SDK’s send_event() method.
  • SDK capture directory: Files written to a special location will emit events.
  • Antithesis environment: Systems like our fault injector, journald, and others will emit events. See their documentation for details.

Event fields

Events have the following fields (optional where noted):

  • event.moment: The moment at which an event happened.

  • event.moment.vtime: The time that passed within the simulation along this timeline, measured in seconds.

  • event.source: The source that emitted an event.

  • event.source.name: The human-readable name of the system or process that emitted the event.

    • For stdout/stderr and SDK assertion events it is your container name.
    • For the SDK capture directory, it is effectively the path into that directory and is documented here.
    • For Environment event emitters fault_injector, journal, and background_monitor it is the that emitter name.
  • event.source.pid: Optional. The Linux process ID of the process that generated this event. PIDs are available for events emitted by the Antithesis SDK, or via writes to the SDK capture directory.

  • event.source.container: Optional. The hostname of the container that generates this event, if any.

  • event.source.stream: Optional. Current values include "error", "info", "internal". For stdout/stderr this maps to "info" and "error" respectively.

  • event.output_text: Optional. The text-payload of the event. For stdout events this will be the verbatim text, with one event per newline.

  • event.exit_code: Optional. If present, means that the Antithesis environment has terminated with an exit code. This is an error in the Antithesis platform, please email support@antithesis.com to let us know.

Helper method

add_fields

add_fields is a helper method on an event, not a standalone verb. It returns a new synthetic event with the given fields added, leaving the original event unchanged. Use it inside the expression of map, flatmap, or fold, which you’ll learn below, to annotate events with values you compute.

event.add_fields({field_1: value_1, field_2: value_2, ...})

Each key is the name of a field to add, and each value can be any JSON value (a string, number, boolean, array, or object) or another event. Because it returns the annotated event rather than modifying the original, always use its return value.

If a field name you add already exists on the event, its value is overwritten. But, it can’t change the fields that identify when an event happened, the moment field (including vtime and input_hash) is always carried over from the original event, so passing a moment, vtime, or input_hash field has no effect.

Operations

These are all the operations you can request in the API query. All of these operations can be chained together with a . and are evaluated left to right.

Each operation has an example of how to use it. These examples are based on the same system under test — a cluster of database nodes (FoundationDB in this case) and a few workload containers — but you don’t need to know anything about either databases or the workload to understand the examples.

String-matching operations

These operators match on one of the four fields: source, container, stream, or output_text. These are the flattened forms of some of the underlying Event fields. Each name maps to a specific event field:

Field nameMatches against
output_textevent.output_text
sourceevent.source.name
streamevent.source.stream
containerevent.source.container

If you need to match on any other event field, use filter.

matches

Filter events whose fields exactly match all the given values. When you supply more than one field, an event must satisfy all of them.

matches({field_1: "exact value 1", field_2: "exact value 2", ...})

Example:

matches({container: "fdb-workload-1"})

contains

Filter events whose fields contain all the given substrings. When you supply more than one field, an event must satisfy all of them.

contains({field_1: "substring 1", field_2: "substring 2", ...})

Example:

contains({output_text: "error"})

not_matches

Exclude events whose fields exactly match all the given values.

not_matches({field_1: "exact value 1", field_2: "exact value 2", ...})

When you supply more than one field, e.g., not_matches({container: "fdb-workload-1", stream: "error"}), it excludes events matching both, and keeps the events that matches just one.

Example:

not_matches({container: "fdb-workload-1"})

excludes

Exclude events whose fields contain all the given substrings.

excludes({field_1: "substring 1", field_2: "substring 2", ...})

When you supply more than one field, e.g., excludes({container: "fdb-1", stream: "error"}), it excludes events matching both, and keeps the events that matches just one.

Example:

excludes({container: "workload"})

Filtering and transformation

Unlike the string-matching operations, these operations take an arbitrary JavaScript expression and operate on the Event object.

filter

Filter events where the predicate is truthy.

filter(row => expr)

Example: Filter events from fdb-workload-1 container with vtime greater than 20.

matches({container: "fdb-workload-1"}).filter((event) => event.moment.vtime > 20)

map

Transforms the event fields as per the given expression.

map(row => expr)

Example: Logs from the database server containers report the bytes used by the internal key-value store (KvstoreBytesUsed) and the total bytes available (KvstoreBytesTotal). Get events from fdb-1 where storage utilization exceeds 10%.

matches({container: "fdb-1"}).map(event => event.add_fields({kvstore_used_fraction: Number(event.KvstoreBytesUsed) / Number(event.KvstoreBytesTotal)})).filter(event => event.kvstore_used_fraction > 0.1)

Notice that we use add_fields to annotate each event with the utilization at that moment, which is further used to filter the events when the utilization is above 10%.

narrow

Restrict events to the listed fields (plus a small set of fields that can’t be dropped).

narrow(["field1", "field2"])

Example: Keep only the internal key-value store’s utilization fields.

narrow(["KvstoreBytesTotal", "KvstoreBytesUsed"])

Each returned event has only these two fields plus the fields that can’t be dropped.

{
"IPT_bytes_out":57763520,
"KvstoreBytesTotal":"943562752",
"KvstoreBytesUsed":"104886272",
"moment":{
"input_hash":"-4227404350065649544",
"vtime":"185.90551807894371",
"session_id":"9802c35540a7a8bea7441090b9c40d96-56-17"
}
}

flatmap

Transform and flatten the result by one level.

flatmap(row => expr)

fold

A higher-order function that processes a complex data structure and iteratively computes a set of values to attach to each event. Think of a fold function like a reducer, it takes an arbritary expression (a reducer function) to compute one or more values at a given moment and adds corresponding fields to the event at that moment.

fold((state, event) => expr, initial_state)

Example: Imagine the database attaches a severity score to certain logs, and for every severity-30 event you want to know how many severity-20 events preceded it in the same timeline. A fold walks each timeline’s events in order, from the root to the leaf, threading a running count through them; you then keep only the severity-30 events.

fold((sev20_agg, ev) => {
if (ev.Severity == "20") {
sev20_agg = sev20_agg + 1
}
return [[ev.add_fields({sev20_count: sev20_agg})], sev20_agg ]
}, 0)
.filter(ev => ev.Severity == "30")

Note the shape of a fold. The running count of severity-20 events lives in the state variable sev20_agg, initialized to 0. The reducer expression, i.e. the function inside the fold, increments it whenever the current event has severity-20, and annotates every event with this running count via add_fields. It returns a two-element [[event], new_state] tuple, the modified event wrapped in an array, and the state to carry to the next event.

Comparing the example fold with the syntax,

  • state = sev20_agg. The named variable that stores the state.
  • expr = the reducer function inside the fold that contains the logic to count severity-20 events.
  • initial_state = 0. The value passed in the argument after defining expr.

The fold annotates each event with a new field sev20_agg, here’s an example of an annotated event:

{
"ClientDescription": "external-7.3.75-16904100321382952430",
"DateTime": "2026-07-02T02:05:55Z",
"Duration": "0.161613",
"ID": "0000000000000000",
"IPT_bytes_out": 3404487336,
"sev20_agg": 12225,
"LogGroup": "default",
"Machine": "10.89.0.6:138",
"MClocks": "694.121",
"NumYields": "1",
"SampleRate": "1",
"Severity": "30",
"source": {
"container": "fdb-workload-1",
"name": "antithesis/pods/fdb-workload-1/trace.10.89.0.6.138.1782956537.6jcNPm.0.3.json",
"pid": 143
},
"TaskID": "10500",
"ThreadID": "15531480011165514199",
"Time": "1782957955.593818",
"Type": "SlowTask",
"moment": {
"input_hash": "1486441498283514350",
"vtime": "1669.7344137146138",
"session_id": "9802c35540a7a8bea7441090b9c40d96-56-17"
}
}

Here’s an illustration showing how the fold aggregates the count:

Example `fold` operation.
  • Pink dots are the events with a severity score of 20.
  • Green dots are all other events in the test run.

Note that this illustration doesn’t show the filtering part.

Set operations

Each of these operations combines the input event set with one or more event sets passed as arguments. Remember the first operation’s input is all recorded events. So a set operation written as the very first verb combines every recorded event with its arguments. This is useful for difference and intersect, but redundant for union and distinct_by_moment as you’d get everything back. Chain those onto a preceding event set.

union

Combines the input event set with the argument event sets via a logical OR. Identical events (events with all same fields and values) are deduplicated.

event_set.union(pipeline, ...)

Example:

matches({container: "fdb-1"}).union(matches({stream: "error"}))

An event that matches both operands (from container fdb-1 and on the error stream) appears only once in the result.

intersect

Keeps the events present in the input event set and in every argument event set via a logical AND.

event_set.intersect(pipeline, ...)

Example:

matches({container: "fdb-1"}).intersect(matches({stream: "error"}))

difference

Keeps the events in the input event set that are not in the argument event set: A - B. Unlike the set operations above, difference takes exactly one argument.

event_set.difference(pipeline)

Example:

matches({container: "fdb-1"}).difference(matches({stream: "error"}))

distinct_by_moment

Combines the input event set with the argument event sets, similar to union, but keeps only one event per vtime. This is useful when you want to combine it with a synthetic event set which may have one or more added fields, as union would treat it as a different event from the originally recorded event. When several events share a vtime, the one from the earliest event set in the list wins (the input event set is checked first).

event_set.distinct_by_moment(pipeline, ...)

Temporal operations

These operations walk the timeline to relate each event to other events that came before or after it. See the multiverse refresher for how events are ordered across the multiverse tree.

with_last

Annotates each event in the input event set with the closest matching event from earlier in its own timeline. For each input event, with_last looks back through that event’s execution history and finds the nearest event belonging to each named event set in the arguments. It adds one field per named argument, last_{name}, where {name} is the label you gave that event set and field’s value is the matched event.

with_last({name: pipeline, ...})

Example: Imagine the database attaches a severity score to certain logs, and you want all the severity-30 events annotated with the last severity-20 event.

filter(ev => ev.Severity == "30").with_last({sev20: filter(ev => ev.Severity == "20")})

This returns every severity-30 event with an added last_sev20 field holding the nearest earlier severity-20 event in its timeline. Severity-30 events with no earlier severity-20 event are still returned — just without the last_sev20 field. Here’s an example event in the response:

{
"ClientDescription": "external-7.3.75-16904100321382952430",
"DateTime": "2026-07-02T02:05:55Z",
"Duration": "0.161613",
"ID": "0000000000000000",
"IPT_bytes_out": 3404487336,
"last_sev20": {
"BackgroundThread": "0",
"ClientDescription": "external-7.3.75-14346399803906717311",
"DateTime": "2026-07-02T02:09:15Z",
"ErrorCode": "113",
"ErrorMsg": "No route to host",
"ID": "eda58cbdc5e3e4dc",
"IPT_bytes_out": 3404483920,
"LogGroup": "default",
"Machine": "10.89.0.6:788",
"Severity": "20",
"source": {
"container": "fdb-workload-1",
"name": "antithesis/pods/fdb-workload-1/trace.10.89.0.6.788.1782956780.EI6xhv.0.2.json",
"pid": 833
},
"SuppressedEventCount": "4",
"ThreadID": "1305798038123074769",
"Time": "1782958155.683198",
"Type": "N2_ConnectError",
"moment": {
"input_hash": "1486441498283514350",
"vtime": "1669.7319037609268",
"session_id": "9802c35540a7a8bea7441090b9c40d96-56-17"
}
},
"LogGroup": "default",
"Machine": "10.89.0.6:138",
"MClocks": "694.121",
"NumYields": "1",
"SampleRate": "1",
"Severity": "30",
"source": {
"container": "fdb-workload-1",
"name": "antithesis/pods/fdb-workload-1/trace.10.89.0.6.138.1782956537.6jcNPm.0.3.json",
"pid": 143
},
"TaskID": "10500",
"ThreadID": "15531480011165514199",
"Time": "1782957955.593818",
"Type": "SlowTask",
"moment": {
"input_hash": "1486441498283514350",
"vtime": "1669.7344137146138",
"session_id": "9802c35540a7a8bea7441090b9c40d96-56-17"
}
}

Here’s a visual illustration of how the last events are picked:

Example `with_last` temporal operation.
  • The coral dots form the input event set, in this example, it’s the events with severity = 30. This is the event set that’ll be annotated with last severity-20 event (if exists) and returned.
  • The blue dots form the argument event set, sev20, which are event with severity = 20.
  • The dotted arrows mark the relations of severity-30 events and the last severity-20 events in their execution history.
  • The yellow dotted circle marks the severity-30 events which do not have any severity-20 events in their execution history.

with_next

For each event in the input event set, with_next looks forward along that event’s timeline for the closest following event belonging to one of the named event sets in the arguments. The result is those matched (following) events, each annotated with two fields: last_event, the input event it follows, and with_next_type, the label of the named set it matched.

This is the inverse of with_lastwith_last returns the input events annotated with a past match, whereas with_next returns the matched events annotated with the input event that preceded them.

The look-ahead can be bounded with an optional timeout (in seconds). For example, if the timeout is 10, an input event is matched only against events following within 10 seconds of it. When the timeout elapses with no match, with_next emits a synthetic event tagged with_next_type: "timeout".

with_next({name: pipeline, ...}, timeout_sec)

Example: The inverse of the with_last example — for each severity-20 event, find the next severity-30 event that follows it in the same timeline.

filter(ev => ev.Severity == "20").with_next({sev30: filter(ev => ev.Severity == "30")})

This returns all the severity-30 events, annotated with an additional field last_event and with_next_type. Here’s an example event in the response:

{
"DateTime": "2026-07-02T01:47:51Z",
"ID": "fbd2f532b6a921df",
"IPT_bytes_out": 78978456,
"last_event": {
"BackgroundThread": "0",
"DateTime": "2026-07-02T01:47:51Z",
"ErrorCode": "113",
"ErrorMsg": "No route to host",
"ID": "acf3845656d68c7e",
"IPT_bytes_out": 78976232,
"LogGroup": "default",
"Machine": "10.89.0.28:4500",
"Roles": "CD,CS,DD,MS,RK,SS,TL",
"Severity": "20",
"source": {
"container": "fdb-1",
"name": "antithesis/pods/fdb-1/trace.10.89.0.28.4500.1782956792.s7R821.0.1.json",
"pid": 31
},
"SuppressedEventCount": "3",
"ThreadID": "10323156958324524988",
"Time": "1782956871.192063",
"Type": "N2_ConnectError",
"moment": {
"input_hash": "7311843931998727955",
"vtime": "382.8245171129238",
"session_id": "9802c35540a7a8bea7441090b9c40d96-56-17"
}
},
"LogGroup": "default",
"Machine": "10.89.0.28:4500",
"Roles": "CD,CS,DD,MS,RK,SS,TL",
"Severity": "30",
"source": {
"container": "fdb-1",
"name": "antithesis/pods/fdb-1/trace.10.89.0.28.4500.1782956792.s7R821.0.1.json",
"pid": 31
},
"SuppressedEventCount": "21",
"ThreadID": "10323156958324524988",
"Time": "1782956871.362784",
"Type": "RkSSListFetchTimeout",
"with_next_type": "sev30",
"moment": {
"input_hash": "7311843931998727955",
"vtime": "382.82451861328445",
"session_id": "9802c35540a7a8bea7441090b9c40d96-56-17"
}
}

Here’s a visual illustration of how the next events are picked:

Example `with_next` temporal operation.
  • The blue dots form the input event set, in this example, it’s the events with severtiy = 20.
  • The coral dots form the argument event set, sev30, which are are event with severity = 30. This event set will be annotated with the last severity-20 event, the event set label, as supplied in the argument, and returned.
  • The dotted arrows mark the relations of severity-20 events and the next severity-30 events in their execution history.
  • The yellow dotted circle marks the severity-20 events which do not have any severity-30 events in their future timeline.