Docs/Developer/Compile & Query Workflow
DeveloperConcept

Compile & Query Workflow

Atlas separates ontology authoring from query execution with an explicit compile step.

Updated 29 Oct 2026Timebase Docs

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 detail is not the length of violations[]. Each entry carries an occurrences field saying how many violations it stands for, which is almost always 1 but is larger on a summary entry standing in for a bounded fan-out. detail reports the sum. So "24431 violation(s) found in lonestar." alongside forty array entries is correct, not a bug. If you total the array yourself, sum occurrences.

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 main does not affect queries on branch feature/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 sum occurrences. One broken reference can fan out over every expanded row of an instance map, so violations[] is a sample rather than the full account. errorCount and a group's count are 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.

esc
↑↓ navigate · ↵ open · ⌘K anywhere