Compile & Query Workflow
Atlas separates ontology authoring from query execution with an explicit compile step.
Overview#
Atlas separates ontology authoring from query execution with an explicit compile step. You define your ontology and instance maps via the API, then build a World to make that definition queryable. This article explains when to compile, what the compile response tells you, and how queries relate to the compiled state.
The workflow is four REST operations:
| Route | Purpose |
|---|---|
PUT /api/v1/branches/{branch}/domains/{domain}/world |
Build (or rebuild) the compiled World for a domain |
GET /api/v1/branches/{branch}/domains/{domain}/world |
Read compile status and staleness without recompiling |
POST /api/v1/branches/{branch}/domains/{domain}/queries |
Run a Cypher query against a compiled World |
GET /api/v1/branches/{branch}/domains/{domain}/validation |
Check for violations without compiling |
A fifth route discovers which worlds exist:
| Route | Purpose |
|---|---|
GET /api/v1/worlds |
List every compiled World in this Atlas process |
For the request and response shapes, including how compile failures surface, see Atlas API - Compile and Query (the API reference). This article focuses on when to compile and how the workflow fits together.
Building the World (compile)#
PUT /api/v1/branches/{branch}/domains/{domain}/world
No request body. The operation is idempotent: every call triggers a full recompile that replaces any previously cached World. It is also synchronous - the World is built inside the request and the report comes back in the same response.
curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
"https://atlas.example.com:4552/api/v1/branches/main/domains/lonestar/world"
Success - 200 OK
{
"entityCount": 634,
"edgeCount": 73,
"archetypeCount": 42,
"compiledAt": "2026-08-07T14:22:31.482Z",
"functionErrors": []
}
Refused - 403 Forbidden, with an error code naming Types or Instances
A compile you ask for is weighed against the installation's licence before the world is published: the compiled types and instances this world would add to the running total are checked against the Types and Instances entitlements, and the compile is refused if either ceiling would be breached. Nothing is stored and the previously cached world, if any, is untouched. The remedy is in your hands: unload a domain you do not need compiled (below), or model within the limit, then compile again. The recompile Atlas runs for itself at startup is not gated, so a restart never leaves an arbitrary subset of your domains uncompiled. See Licensing and Entitlements.
| Field | Meaning |
|---|---|
entityCount |
Instances in the compiled World. An instance appears once per property-strategy occurrence (bound properties contribute multiple entries). |
edgeCount |
A-Box edges between those instances. |
archetypeCount |
T-Box classes (types) the World defines. This count never varies with instance data - it is a per-type count of archetypes in the ontology. |
compiledAt |
UTC timestamp when this compile completed. |
functionErrors |
Ontology functions that failed to compile to executable code. Empty on a clean build. See Validation Rules Reference for function validation details. |
Compilation is not incremental - every call re-reads the files, re-validates, rebuilds the graph from scratch and replaces whatever the cache held. For a large ontology this overhead is unavoidable; there is no "nothing changed, skipping" fast path.
Failure - 422 Unprocessable Entity
If compilation validation fails, you get a 422 with Content-Type: application/problem+json and a violations[] array:
{
"type": "https://atlas.timebase.io/errors/validation-failed",
"title": "Validation failed",
"status": 422,
"detail": "3 violation(s) found in lonestar.",
"violations": [ ... ]
}
When violations are present, no World is built at all. The previously compiled World continues to answer queries. The violations[] array holds every Error-severity violation found.
The count in
detailis not the length ofviolations[]. Each entry carries anoccurrencesfield saying how many violations it stands for, which is almost always1but is larger on a summary entry standing in for a bounded fan-out.detailreports the sum. So"24431 violation(s) found in lonestar."alongside forty array entries is correct, not a bug. If you total the array yourself, sumoccurrences.
For the field details and how to read a violation, see Validation Rules Reference.
When to compile#
Compile after every batch of changes that you want to be queryable. The rule is simple: queries always run against the last successfully compiled state, not the current ontology files. If you have made any write to the ontology or instance maps since the last compile, queries will reflect the state at the previous compile, not the current one.
Common patterns:
| Scenario | Compile timing |
|---|---|
| Interactive ontology editing | Compile once when ready to test queries |
| Import pipeline (many nodes/properties) | Compile once at the end, after all writes |
| CI/CD validation | Check for violations first, then compile as the final step |
| Single property change | Compile immediately if queries need the new value |
Do not compile between individual writes in a batch. Complete all writes first, then compile once.
What triggers a compile#
An explicit compile (PUT /api/v1/.../world) is the primary path. But you should know about two others:
Atlas recompiles at startup#
The Atlas process runs StartupAutoRecompileService on boot, which automatically recompiles every (branch, domain) pair that was successfully compiled before the restart. The records are persistent across restarts, stored in {DataDirectory}/compiled-worlds.json. So nothing is queryable until you compile, but after a restart, your previously compiled domains are queryable again without manual action.
Connector-dependent domains are deferred. If a domain's instance maps use RowsetTemplate definitions, their expansion requires a connector to advertise. These domains are not compiled at boot; they are recompiled one-shot when their declared connector advertises. A domain whose connector never advertises stays not-compiled for the rest of the process.
A compile from inside a draft does not update the shared cache#
While you hold a draft session, a compile of your draft runs normally and produces a full CompileResult, and leaves the world that everyone else queries in place. This prevents leaking one author's unpublished, possibly-invalid work into everyone's query results. Once you publish the draft, the draft IS the branch and an ordinary compile will cache it. See Draft Sessions for the full story.
Checking compile status without recompiling#
GET /api/v1/branches/{branch}/domains/{domain}/world
Returns the cached World's status and staleness without triggering a recompile. Use this when you want to know whether a domain is queryable and whether a recompile is needed.
Response - 200 OK
{
"entityCount": 634,
"edgeCount": 73,
"archetypeCount": 42,
"compiledAt": "2026-08-03T09:41:07.221Z",
"isStale": false,
"compiledAtSha": "7f3d0884",
"currentSha": "7f3d0884"
}
| Field | Meaning |
|---|---|
entityCount, edgeCount, archetypeCount, compiledAt |
Same as the compile response. |
isStale |
true when the domain's files have changed since the compile. false when the compile is current. Computed from git tree object IDs across the domain subtree plus the transitive library closure, so it is always accurate. If compiledAtSha == "", staleness is unknown and the field is always true. |
compiledAtSha |
The git commit SHA the World was compiled from. |
currentSha |
The current HEAD commit of the branch. When both are equal, the World is current. |
This replaces hand-rolled "should I recompile?" heuristics. When isStale is false, queries will reflect the current files.
Not compiled - 400 Bad Request
If the domain has never been compiled on this branch, you get a 404-shaped error with code Query.NotCompiled:
{
"type": "https://atlas.timebase.io/errors/invalid-request",
"title": "Invalid request",
"status": 400,
"code": "Query.NotCompiled",
"detail": "Domain 'lonestar' on branch 'main' has not been compiled. Call PUT /api/v1/branches/main/domains/lonestar/world first."
}
Discovering compiled worlds#
GET /api/v1/worlds
Lists every (branch, domain) pair currently held in the world cache. Only compiled worlds appear, so if you pick a target from this list you will never hit Query.NotCompiled on your first query.
| Query parameter | Type | Description |
|---|---|---|
branch |
string | Optional. Case-sensitive exact match on the branch name. |
domain |
string | Optional. Case-sensitive exact match on the domain name. |
Response - 200 OK
{
"items": [
{
"branch": "main",
"domain": "lonestar",
"compiledAt": "2026-08-03T09:41:07.221Z",
"entityCount": 634,
"edgeCount": 73,
"archetypeCount": 42,
"isStale": false,
"compiledAtSha": "7f3d0884",
"currentSha": "7f3d0884"
}
]
}
This is the in-memory cache contents - a ConcurrentDictionary per Atlas process. Restarting empties it; it refills as StartupAutoRecompileService recompiles the recorded domains.
Unloading a domain#
DELETE /api/v1/branches/{branch}/domains/{domain}/world
The third verb on the world resource: PUT builds, GET reads, DELETE unloads. Unloading removes the compiled world from the cache, so the domain stops answering queries, stops holding memory, and stops counting against the licensed Instances entitlement. The domain's files are untouched; recompiling is the whole of the undo.
The call is idempotent and always answers 204 No Content, whether or not a world was cached, so a scripted teardown never has to special-case "already unloaded". The durable record that drives the startup recompile is removed with the world, so an unloaded domain stays unloaded across a restart until something compiles it again.
Two other events unload worlds without being asked:
- Deleting a domain evicts its compiled world. Before this was so, a deleted domain stayed compiled, queryable and metered until a restart.
- Deleting a branch evicts every world compiled on that branch, and a discarded draft session evicts anything cached under its draft reference.
Each of those frees the licensed allocation the way an explicit unload does.
Queries and Compiled State#
Queries always run against the last successfully compiled state, not the live ontology definition files. This means:
- Changes made after the last compile are not visible to queries until you compile again
- If a compile fails, the previous compiled state remains active and queries continue to work against it
- Compiling on branch
maindoes not affect queries on branchfeature/my-work- each branch has its own compiled state
Stale state behaviour#
If you query a type that was added after the last compile:
MATCH (n:NewNodeType) RETURN n.Label
This returns an empty result, because a type name that matches nothing returns no rows rather than an error. If a query returns no rows, check GET .../world to see whether a recompile is needed.
Running a query#
POST /api/v1/branches/{branch}/domains/{domain}/queries
Execute a Cypher query against a compiled World. For the language itself and the conventions that shape a result, see openCypher Query Reference.
Request
{
"cypher": "MATCH (n:Pump) WHERE n.FlowRate.Value > $min RETURN n.Label, n.FlowRate",
"parameters": { "min": 100 }
}
| Field | Required | Type | Description |
|---|---|---|---|
cypher |
yes | string | The Cypher query string |
parameters |
no | object | Named query parameters, accessed as $name in the query |
projections |
no | boolean | Set to true to get enrichment; false (or omitted) for bare results |
Response - 200 OK (bare)
{
"columns": ["n.Label", "n.FlowRate"],
"data": [
["Pump-01", { "Timestamp": "2026-08-07T19:35:45.513939Z", "Value": 150.5, "Quality": 192 }],
["Pump-02", { "Timestamp": "2026-08-07T19:35:40.000000Z", "Value": 200.3, "Quality": 192 }]
]
}
Response - 200 OK (with "projections": true)
Adds two fields:
{
"columns": ["n"],
"data": [ ... ],
"classesTouched": ["urn:atlas:ontology:lonestar-plant:Pump"],
"instanceIdsByClass": {
"urn:atlas:ontology:lonestar-plant:Pump": ["Pump-01", "Pump-02"]
}
}
| Field | Meaning |
|---|---|
classesTouched |
Full URNs of every entity type that appears in the result rows, in ordinal (case-sensitive) order. Empty when the result contains no entity-valued columns. |
instanceIdsByClass |
Maps each type URN to a list of instance IDs (durable URN strings) of that type that appear in the results. Use these to drive UI components, filter views, or correlate results across multiple queries. The URN strings are durable across recompiles. A type can appear with an empty URN list if the query matched edge nodes or strategy nodes with no URN. |
Not compiled - 400 Bad Request
If the domain has never been compiled, you get a 400 with code Query.NotCompiled. You must compile first.
Query errors - 400 Bad Request
If the Cypher is malformed or semantically invalid, you get a 400 with code Query.SyntaxError, Query.SemanticError or Query.Unsupported. Every error carries the code extension; the type URI identifies the error family.
Validation without compiling#
GET /api/v1/branches/{branch}/domains/{domain}/validation
Check for violations without compiling or caching anything. Use this in your fix-and-recheck loop - it is cheaper than a compile and it changes nothing.
Response - 200 OK
{
"branch": "main",
"sha": "7f3d0884",
"errorCount": 3,
"warningCount": 0,
"blocking": false,
"violations": [
{
"ruleId": "I-B3",
"severity": "Error",
"filePath": "instances/equipment.map.json",
"jsonPointer": "/3/properties/urn:atlas:ontology:plant:FlowRate",
"instanceId": "pump-01",
"message": "...",
"suggestion": null
}
]
}
| Field | What it tells you |
|---|---|
branch |
The branch you asked about. |
sha |
The commit that was actually checked. If you own a draft session that is ahead of the branch, this is your draft's commit. Everybody else sees the branch. |
errorCount |
Error-severity violations. This is your to-do list. |
warningCount |
Warning-severity violations (informational, never blocking). |
blocking |
true when a fatal rule (O-A1 or O-A2) fired, which also means the list is truncated - more problems may be hidden behind it. |
violations |
A capped sample in pipeline order, 500 at most. Each carries the same fields as a compile failure, occurrences included. |
groups |
Every violation collapsed by rule, severity, file and definition, each with a true count. This is what to build on. Ordered biggest problem first, capped at 1,000. |
omittedViolationCount, omittedGroupCount |
What each cap left out, so you can say "and N more" honestly rather than truncating in silence. |
Build on
groups, and sumoccurrences. One broken reference can fan out over every expanded row of an instance map, soviolations[]is a sample rather than the full account.errorCountand a group'scountare true totals either way. Validation Rules Reference has the group shape in full.
This is the only place Warning-severity violations are visible. The compile response carries only the outcome; warnings never block the compile, so they do not appear in a 200 OK.
For detailed rule explanations and how to read a violation, see Validation Rules Reference. That article owns the validation rule catalogue - do not duplicate the rule table in other docs.
Compile in a CI/CD Pipeline#
A typical validation pipeline pattern:
import requests
BASE = "https://atlas.example.com:4552"
HEADERS = {"Authorization": f"Bearer {token}"}
BRANCH = "main"
DOMAIN = "PlantModel"
# 1. Apply all ontology changes (nodes, properties, relationships, instance maps)
# ... your write requests ...
# 2. Check for violations (without compiling - fast, read-only)
resp = requests.get(
f"{BASE}/api/v1/branches/{BRANCH}/domains/{DOMAIN}/validation",
headers=HEADERS
)
resp.raise_for_status()
status = resp.json()
errors = status["errorCount"]
if errors > 0:
print(f"Found {errors} error(s). Run compile to see details.")
# Report groups, not `violations`: that list is a capped sample, while
# `groups` accounts for every violation and carries a true count.
for g in status["groups"]:
if g["severity"] == "Error":
print(f" [{g['ruleId']}] x{g['count']} {g['filePath']} - {g['messages'][0]}")
if status["omittedGroupCount"] > 0:
print(f" ...and {status['omittedGroupCount']} more distinct problems")
raise SystemExit("Fix errors before compiling")
# 3. Compile
resp = requests.put(
f"{BASE}/api/v1/branches/{BRANCH}/domains/{DOMAIN}/world",
headers=HEADERS
)
if resp.status_code != 200:
# Compile failure with 422 means validation violations
print(f"Compile failed: {resp.json()['detail']}")
raise SystemExit("Compile produced errors")
result = resp.json()
print(f"Compiled OK: {result['entityCount']} entities in {result['archetypeCount']} types")
# 4. Optionally run a smoke-test query
resp = requests.post(
f"{BASE}/api/v1/branches/{BRANCH}/domains/{DOMAIN}/queries",
headers=HEADERS,
json={"cypher": "MATCH (n) RETURN count(n) AS total"}
)
resp.raise_for_status()
print(f"Instance count: {resp.json()['data'][0][0]}")
How compile reads are isolated#
Every compile is a snapshot of a single git commit - the commit SHA is stored in compiledAtSha and returned by GET .../world. Atlas reads that commit using in-process git object store access (libgit2), not a worktree checkout. This means:
- A compile is immune to concurrent writes on the same branch (they do not interfere with the read)
- Reads do not lock or block writes
- Each compile's snapshot is durable and reproducible
Writes still use a worktree + per-branch lock for atomicity. This architecture is why compiledAtSha is meaningful - it precisely identifies which commit was compiled.
Quick Reference#
| Action | Endpoint | Notes |
|---|---|---|
| Build the World | PUT .../world |
Idempotent; every call is a full rebuild. No body. |
| Read status | GET .../world |
Returns isStale and compiledAtSha. No rebuild. |
| List all Worlds | GET /api/v1/worlds |
In-memory cache; refills on startup. |
| Query | POST .../queries |
Always against the last successful compile. |
| Check for errors | GET .../validation |
Cheap read; shows errors and warnings. |
| Publish draft | POST /api/v1/branches/{branch}/draft/publish |
Moves draft work to the branch so compiles cache it. |
Related articles#
- Atlas API - Compile and Query - the complete request/response shapes and status codes
- Validation Rules Reference - what every rule ID means and how to fix it
- openCypher Query Reference - the dialect and the conventions that shape a result
- Querying with Cypher - end-user guide to modelling queries and solving common problems
- Compiling and Publishing a Domain - how write validation and draft sessions interact (