Investigating changes with audit logs
Reconstruct who changed an entity, what they changed, and when with entityHistory and auditEvents queries.
Business Data Repository records creates, updates, and deletes on eleven entity types, listed below. You read that record through two queries. This scenario walks you through a compliance investigation: an asset's configuration changed unexpectedly, and you need to establish who changed it, what the old and new values were, and what else that actor touched in the same session.
By the end, you'll be able to read one entity's full change history, decode the stored before-and-after values, widen the search across a whole workspace, and tie an audit entry back to your own application logs.
Prerequisites
You need an access token for the workspace you work in. The API takes the workspace from the token, so no operation on this page asks for a workspace ID. See Authentication for how to get a token and how the API picks the workspace.
You also need an audited entity that has been changed at least once. The first step below shows how to find its ID, including when the entity was already deleted. See Which entities are audited for the list of audited entities.
How audit works
The API writes every recorded change as an audit event, and events never change after they're written. Business Data Repository keeps events with no time limit. This section covers what one event holds, which entities produce events, and how to read the recorded values.
What one event records
An audit event describes the actor, the affected entity, the type of change, the before and after values, and the details of the request, so you can match the change against your own logs.
actor
Who made the change: a User, an Integration, or a SystemActor.
aggregateType / aggregateId
Which entity changed, as a type code plus its ID.
eventType
The type of change: CREATED, UPDATED, DELETED, or RESTORED.
eventData
The changed fields with their old and new values, plus the entity's stored state before and after the change.
occurredAt
When the change happened. It comes from the event ID, so ordering by time always matches ordering by ID.
traceId
The W3C trace ID of the request or other work that caused the change, for matching audit events with application logs.
ipAddress / userAgent
Where the request came from.
sourceType
The kind of source the change came from. Every event has API.
workspace
The workspace the change belongs to, never null. Both queries return only events from your token's workspace, including changes the platform made in it on its own. To spot those, check whether actor is a SystemActor.
Which entities are audited
Eleven entity types are recorded. aggregateType always contains one of these codes, and no other value:
asset
CREATED, UPDATED, DELETED
asset_group
CREATED, UPDATED, DELETED
catalog_item
CREATED, UPDATED, DELETED
custom_field_definition
CREATED, UPDATED, DELETED
device
CREATED, UPDATED, DELETED
geo_object
CREATED, UPDATED, DELETED
integration
CREATED, UPDATED, DELETED
inventory
CREATED, UPDATED, DELETED
member
CREATED, UPDATED, DELETED
schedule
CREATED, UPDATED, DELETED
workspace
CREATED, UPDATED, DELETED, RESTORED
catalog_item covers every catalog item, such as asset types, tags, and catalogs. Archiving a custom field definition and restoring it with the restore action are both recorded as UPDATED, because neither deletes the definition. RESTORED is recorded only for a workspace that Navixy Console reactivates.
What eventData contains
eventData holds the answer to the investigator's main question: what exactly changed. The other event fields say who changed which entity and when, while eventData holds the values themselves, so you can see the old and the new state without any other source. It's a JSON scalar with two keys:
changedFields holds one entry per field whose value differs, each with its old and new value. Every update sets a new updatedAt, so on an UPDATED event changedFields always lists at least that field.
additionalContext holds the wider context. operation names what happened (CREATE, UPDATE, DELETE, or RESTORE). before_state and after_state hold the entity's stored state immediately before and immediately after the change. Where changedFields shows only what changed, these two show the whole record.
The keys in changedFields and in both states are the property names that Business Data Repository stores internally, not the GraphQL field names. For example, an asset's type is stored as assetTypeId, and all its custom field values are in one key, customFieldsData. A reference to another entity is stored as that entity's ID. The API doesn't guarantee these keys, so use them to read a change rather than in automated logic.
Which of the two you get depends on the operation, because a newly created entity has no "before" and a deleted one has no "after":
Operation
before_state
after_state
changedFields
CREATE
absent
present
every field that has a value, with "old": null
UPDATE
present
present
only the fields that differ
DELETE
present
absent
every field that had a value, with "new": null
RESTORE
present
present
only the fields that differ
eventData is plain JSON rather than a typed GraphQL object, so you can't select individual keys inside it in the query. Request it as one field and read the values in your own code.
How to track a logged change
Find the ID of the entity to investigate
Both audit queries work with entity IDs, so start by finding the ID of the entity you're investigating. List queries take filters that narrow down the output by what you already know, such as a name or a hardware identifier. For example, to find a truck by a fragment of its name, run this query:
Response:
The same pattern works for the other audited entity types: narrow down devices by identifierContains (a fragment of an IMEI or serial number) or titleContains and use titleContains for geo objects, schedules, and asset groups.
If the entity was deleted, list queries no longer return it, but the audit trail still knows it. Search the deletions of its type and take aggregateId from the matching event:
The response lists one entry per deleted asset. aggregateId is the deleted entity's ID:
Either way, the ID you found is the entity ID for the rest of the scenario.
Read the entity's history
Start with entityHistory, which returns every audit event for one entity, newest first:
Response:
The default ordering is { field: OCCURRED_AT, direction: DESC }, so the most recent change is first. That's the one to look at.
actor is the Actor interface. Select id and title directly, or use an inline fragment such as ... on User { email } when you need type-specific fields.
Identify the changes
Add eventData to see what changed, field by field. Narrow down the response to the single event you're looking for with the aggregateIds and eventTypes filters:
Response:
changedFields shows the answer directly: Anna Weber renamed the truck. The updatedAt and version entries appear alongside it because every successful update of an asset sets a new updatedAt and increases the version that optimistic locking checks.
Widen the search to the entire workspace
You now know who made the change. To find out what else that actor did around the same time, switch to auditEvents, which covers the whole workspace rather than one entity, and narrow it down by actor and period:
Response:
Switching direction to ASC puts the events in the order they happened, which is what you want when retracing a sequence of actions.
Note that the first two events share a traceId. That means they came from the same request: renaming the asset and deleting a device happened in one operation.
Match audit events with your application logs
traceId is the W3C trace context trace ID from the request's traceparent header, when that header is present and well-formed. If the header is missing or malformed (wrong version, uppercase hex, an all-zero ID), the API generates a trace ID instead. Either way, you see the ID in the event's traceId and in extensions.traceId of any error the request raised. A generated ID has no match in your own logs, since your client never sent it.
A change that the platform makes on its own has a SystemActor as actor and the trace ID of what caused it, such as a request or a command from Navixy Console. A scheduled run, or a Console command sent without a trace ID, gets a generated trace ID.
One API request can change several entities, and every event it produces has the same traceId. Filtering by it returns that whole set:
When your client sends the traceparent header on the request, the same ID appears in your own logs and tracing tools, so it's the shared value that connects the audit trail to them. Use it in the other direction too: given a suspicious request in your logs, this query tells you exactly which entities it modified.
Filtering
AuditEventFilter applies to both queries. Values within one field are combined with OR, and separate fields are combined with AND. Empty arrays are ignored.
actorIds
[ID!]
Restricting to specific users or integrations
aggregateTypes
[Code!]
Restricting to entity types, such as ["asset", "device"]
aggregateIds
[ID!]
Restricting to specific entity IDs
eventTypes
[AuditEventType!]
Restricting to CREATED, UPDATED, DELETED, or RESTORED
sourceTypes
[SourceType!]
Every event has the source type API
traceId
String
Grouping every change made by one request
from / to
DateTime
Limiting the period
For example, to see every deletion of an asset or a device in one day, run this query:
Ordering and pagination
OCCURRED_AT is the only field you can order by, in either direction. Both queries return an AuditEventConnection and follow the standard cursor pagination described in Pagination:
Request total { count } only when you need to show a total, because counting every matching event is a second database query in addition to fetching the page. To move to the next page, check pageInfo.hasNextPage instead. It tells you whether another page exists and costs nothing extra.
You can also fetch a single audit event through the node query, since AuditEvent implements Node:
Error handling
Audit queries are read-only, so they return the standard errors documented in Error handling.
entityId isn't a valid ID, or first or last is out of range
Check the ID format, and keep first and last between 1 and 100
UNAUTHORIZED (401)
The token is malformed, expired, or issued by another identity service. A request with no token gets PERMISSION_DENIED.
Re-authenticate, see Authentication
How to handle an empty result set
An empty nodes array is a normal response, not an error. Work through the likely causes in this order:
The filter matches no recorded value. Every event has the source type
API, so asourceTypesfilter withoutAPIreturns nothing.RESTOREDis recorded only for a workspace.The change isn't audited. Only the eleven types listed above produce events, and some changes aren't recorded at all, such as a change to a user account or an asset group membership. See Which entities are audited.
The period is wrong.
fromandtoare compared againstoccurredAtin UTC. Make sure your values are in UTC too.The entity belongs to another workspace.
entityHistoryonly searches the workspace that your token names, so an entity from a different workspace returns nothing.
See also
Audit: Complete reference for all audit operations and types
Error handling: Understand error structure, codes, and common error scenarios
Pagination: Page through large result sets with cursors
Optimistic locking: Prevent concurrent updates from overwriting each other with
version
Last updated
Was this helpful?