Mutations and Loads
Mutation statements live inside a named .gq query. Run them with
Mutation statements live inside a named .gq query. Run them with
omnigraph mutate.
query hire($person_id: String, $company_id: String, $role: String) {
insert WorksAt {
from: $person_id,
to: $company_id,
role: $role
}
}Edge endpoints use the reserved assignments from and to; their values are
the logical ids of existing endpoint nodes.
Statements
insert Person { email: $email, display_name: $name }
update Person set { display_name: $name } where email = $email
delete Person where email = $emailA where on update or delete takes the same expressions as a read filter
(see Boolean expressions and nulls):
and, or, not, parentheses, the six comparisons, starts_with,
contains, in, is null and is not null. Its operands are the target type's
properties, named bare, the system fields @id, @src and @dst, literals,
parameters, and now(); from and to stay accepted for @src and @dst.
It keeps only the rows whose expression is true. On four Knows edges, ab1
and ab2 from a to b, ac, and db:
query exact() { delete Knows where @src = "a" and @dst = "b" }removes ab1 and ab2 and nothing else: affected: nodes=0 edges=2. A
binding variable in a where is refused at compile time (T14); an
aggregate, search, or ranking call is T44, for example T44: `count` cannot appear in a mutation where; a where compares the row's own properties, parameters and now(). A compound where runs under either engine setting.
Assignment values are constants: literals, parameters, now(), and
comparisons or Boolean operators over them, such as adult: true or $flag.
A constant's value is fixed per invocation: parameters and now() are bound
once before any retry, so a retried mutation computes the same value. It follows the read rules
for null: with $flag null, true or $flag is true and false and $flag
is false, and $age > 30 with $age null is null. A null result assigned
to a nullable property writes null; on a Blob property it clears the cell.
null is a reserved word, so clear a Blob through a nullable parameter such
as $content: Blob?. Assigned to a non-nullable property a
null result is refused with a typed error that names the property, never
written as a default. A
property or system field in a value is refused, for example T45: `age` cannot appear in an assignment value; assignments and binding matches are constants per invocation.
A mutation query may contain several inserts and updates, or several deletes, but it cannot mix inserts or updates with deletes. Split that workflow into two queries, or run the queries on a branch and merge when the combined result is ready.
Atomicity
An effectful mutation query publishes one graph commit. All of its statements become visible together, or none do. Separate mutation commands are separate commits, even when they run consecutively.
With --json, a successful effectful mutation returns commit with the
exact graph_commit_id and commit metadata published by that attempt. A
successful mutation that matches no entities publishes no commit and returns
"commit": null. Load JSON responses use the same exact receipt.
For a multi-command workflow, use a branch as isolated staging. Earlier changes remain committed on that branch; merging makes the resulting branch state visible on the target in one atomic step. See Branches, Commits, and History.
Insert and update identity
- Inserting a node or edge with
@keyis an upsert by its derived id. Inserting the same key again updates the existing row. - Inserting a node without a key is a strict insert with a generated or supplied id.
- Inserting an edge without a key is a strict insert with a generated or supplied id; the same pair inserted twice is two edges.
- Key properties cannot be changed by an update. Edge types do not support update at all: change a keyed edge by inserting its key again with the new values (an upsert), and an unkeyed edge by delete and re-insert.
All declared value, uniqueness, endpoint, and cardinality constraints are checked before publication.
Bulk loading
omnigraph load accepts newline-delimited JSON. One file can contain nodes and
edges of several types:
{"type":"Person","data":{"email":"ada@example.com","display_name":"Ada"}}
{"type":"Company","data":{"slug":"acme","name":"Acme"}}
{"edge":"WorksAt","from":"ada@example.com","to":"acme","data":{"role":"Engineer"}}Here, Person.email and Company.slug are single-property String keys, so their
derived ids are exactly ada@example.com and acme. The edge uses those ids in
from and to.
A Date value is an integer day count since 1970-01-01 or a YYYY-MM-DD
string; a DateTime value is an integer millisecond count since the Unix epoch
or an ISO 8601 string. Any other JSON type (a float such as 19723.0, a
boolean, an object) fails the load with invalid Date value or invalid DateTime value naming the property. A Date string carries no time of day:
"2024-01-01T02:00:00+05:00" fails the load with invalid Date literal, as it
does in a mutation param, a date(...) literal, or a read filter; an instant
belongs in a DateTime property. A DateTime holds milliseconds: a string with
a non-zero digit past the third fractional digit, such as
"2024-01-01T00:00:00.123456Z", fails the load with invalid DateTime literal,
as it does in a mutation param, a datetime(...) literal, or a read filter.
Trailing zeros, as in .123000, are accepted.
Choose the mode explicitly:
| Mode | Existing id | Use |
|---|---|---|
append | Fails with key_conflict | Add entities without replacing anything. |
merge | Updates the existing entity | Upsert a batch. |
overwrite | Replaces every node or edge type represented in the batch; types absent from the batch remain unchanged | Rebuild from a complete export or seed. |
For a keyed edge the existing-id column applies to its derived id: append
reports key_conflict on an already-committed pair, merge upserts it, and
a supplied top-level id must equal the derivation exactly.
omnigraph load --data batch.jsonl --mode merge graph.omniOne load request is one graph commit. Use --branch <name> --from <base> to
create a missing review branch and load onto it in the same workflow.
Loads preserve supplied embeddings and do not generate them. Results report
embedding_generation: "unsupported" when a loaded node type declares
@embed, or null otherwise; see Embeddings.
Limits and conflicts
Insert/update mutations and incremental keyed loads are bounded to 8,192 entities and 32 MiB per touched type, plus 32 MiB of retained Arrow batches across all touched types in one operation. Keyed loads also have a separate 32 MiB parsed-payload estimate across types. External Blob payloads that require copying count toward the aggregate allowance. Every strict load retains its projected in-memory size check. Blob values have further limits; see Blob limits.
Deletes, including cascades, and overwrite loads collecting replaced IDs have
a separate 32 MiB allowance per operation for those IDs, summed over all
touched types. Each removed ID is charged its UTF-8 length plus 24 bytes, so
the allowance holds 671,088 IDs of 26 bytes, the length of a generated ID. A
delete and the edges it cascades to draw on the same allowance. An overwrite of
entities loaded without a @key and without an explicit id removes every
committed ID of that type, because those IDs are generated again on each load.
Oversized work returns a resource-limit error before its data is staged or published. These checks do not bound total engine memory, and no setting changes them. Split larger inserts, updates, keyed loads and deletes into explicit commits. An overwrite replaces each represented type as one image and cannot be split: it keeps its bulk-input behavior and remains subject to its separate input and removed-ID checks.
Independent existing constructive datasets stage concurrently. The
stage_write_concurrency session setting
controls that width for both Load and insert/update mutations (default 8,
range 1..=64); it is process scope, so the server takes it from
OMNIGRAPH_LOAD_CONCURRENCY and a direct CLI run from that variable or from
--set on load, ingest or mutate (a JSONL input carries no set line),
and an invalid or 0 value refuses startup instead of running the default.
Delete staging remains serial. This affects preparation only—one
request still publishes exactly one graph commit.
A stale strict update, delete, or overwrite can return read_set_conflict.
Refresh the branch and retry deliberately. A key_conflict means an append or
strict insert found an existing id; it never silently becomes an upsert.
If a write returns recovery_required, do not immediately resubmit it. Reopen
the graph read-write or restart the server, then retry from a fresh branch head.
Blobs
Blob assignments accept managed base64: data and, when allowed by graph
policy, external URI references. Ownership differs by load mode, and Blob bytes
count toward write limits. See the canonical Blob guide before
loading them, and its limits for the bounds that apply.
Conditional mutations
Use --if-commit when a mutation should apply only to the graph state that
the caller read:
# The JSON response includes the commit pinned for these rows.
omnigraph query find_person --query queries.gq --store graph.omni --json
omnigraph mutate update_person --query queries.gq --store graph.omni \
--params '{"name":"Ada"}' \
--if-commit <graph_commit_id> --jsonThe condition compares the effective head of the target branch. Any
intervening commit invalidates it, including a commit that changed an unrelated
entity or type. On mismatch, nothing is written: the CLI exits with code 4
against a server and with code 1 on an embedded graph such as this --store
run, and JSON output contains precondition_failure with expected and
optional actual commit ids. Re-read the branch and decide again; do not blindly retry
the old mutation. The precondition is still checked when the mutation would
match no entities.
Use the graph_commit_id returned with the original query rows. Fetching a
head id afterward creates a race between the read and the precondition. For the
HTTP header and dedicated conditional routes, see the
HTTP server guide.