Appendix B: EDN Format
EDN, short for Extensible Data Notation, is the data notation used throughout this guide. Datalevin is written in Clojure, so its native examples use EDN directly: schemas are maps, attributes are keywords, transactions are maps or vectors, Datalog queries are vectors, rules are vectors of lists and vectors, and many examples use tagged literals such as #inst and #uuid.
Readers coming from JSON, SQL, Java, Python, or JavaScript do not need to learn Clojure as a programming language in order to read the examples. EDN is data, not code. It is closer to JSON than to a programming syntax, but with a more regular syntax, richer scalar types, and symbolic names. This appendix gives the EDN subset and Datalevin conventions needed to read and write the examples in the rest of this guide. The official EDN specification is maintained by the edn-format project [1].
1. EDN at a Glance
An EDN value is one readable value. It may be a scalar:
nil
true
false
42
3.14
"hello"
:user/email
?name
#inst "2026-06-09T12:00:00Z"
#uuid "00000000-0000-0000-0000-000000000001"
Or it may be a collection:
[:find ?name :where [?e :user/name ?name]]
{:user/email "ada@example.com"
:user/name "Ada"}
#{:admin :editor}
[(parent ?child ?parent)
[?child :person/parent ?parent]]
Commas are optional whitespace in EDN. The following maps are equivalent:
{:user/name "Ada" :user/age 37}
{:user/name "Ada", :user/age 37}
Datalevin examples usually omit commas because aligned whitespace makes larger schemas and transactions easier to scan.
2. Common Literal Forms
| EDN form | Meaning | Datalevin use |
|---|---|---|
nil |
Absence of a value | Query results, optional arguments, API options. |
true, false |
Booleans | Schema flags such as :db/fulltext true. |
42, -7 |
Integers | Entity ids, counters, ranks, limits. |
3.14 |
Floating-point number | Scores, measurements, vector coordinates. |
"Ada" |
String | Text fields and full-text content. |
:user/email |
Keyword | Attribute names, enum values, options. |
?name |
Symbol | Datalog variables. |
[a b c] |
Vector | Queries, transaction datoms, lookup refs, paths. |
(f x y) |
List | Rule heads and predicate/function clauses in queries. |
{:a 1} |
Map | Schemas, entity maps, idoc documents, options. |
#{:a :b} |
Set | Unordered collections and set-valued data. |
#inst "..." |
Instant | Time values. |
#uuid "..." |
UUID | Stable external identifiers. |
Datalevin-specific caveat: normal datom transactions do not store nil as an attribute value. In transaction maps and [:db/add ...] forms, omit an unknown or inapplicable attribute, or retract the existing datom when a value should disappear. nil is still fine for optional API arguments, missing query results, and ordinary EDN data. idoc documents are the exception: inside a :db.type/idoc value, nil is normalized to :json/null.
The table above shows common EDN literals, not the full Datalevin schema value type reference. Appendix C lists every :db/valueType. A few Datalevin value types deserve special attention when writing portable data:
| Datalevin value type | EDN / portable representation |
|---|---|
:db.type/bigint |
Clojure EDN can carry arbitrary-precision integers, such as 42N, but non-Clojure clients should use the binding's helper or native big-integer type where available. |
:db.type/bigdec |
Clojure EDN can carry arbitrary-precision decimals, such as 3.14M, but portable clients should use binding helpers or native decimal types where available. |
:db.type/bytes |
EDN has no simple portable byte-array literal; use the host binding's bytes value. |
:db.type/tuple |
Use vectors as the value shape, with :db/tupleType or :db/tupleTypes in schema. Composite tuple attributes with :db/tupleAttrs are maintained from component attributes; application code does not transact those tuple values directly. |
:db.type/vec |
Use a numeric vector/array with the client helper expected by the binding; configure dimensions and metric in vector options. |
:db.type/idoc |
Use a nested map value, or a string payload for configured JSON, Markdown, or EDN idoc formats. |
EDN has no object syntax and no assignment syntax. A map is just a collection of key-value pairs. A vector is just an ordered collection. A list is just another ordered collection, though Datalevin often gives lists a special meaning inside query and rule forms.
3. Keywords
Keywords are symbolic values that begin with :. In Datalevin, they are used for attribute names, schema properties, enum-like values, index names, options, and many API flags:
:user/email
:db/valueType
:db.type/string
:order.status/paid
:eav
:closed
:string
:long
A keyword may be unqualified, such as :eav, or qualified with a slash, such as :user/email. The part before the slash is a namespace-like prefix. It does not have to name a Clojure namespace, file, table, or class. It is simply a naming convention that keeps related names together and avoids collisions.
Use qualified keywords for application attributes:
:account/email
:account/created-at
:invoice/total
:invoice/customer
Use unqualified keywords when the surrounding API defines them as local operators, modes, flags, or type descriptors. Datalevin's key-value API uses unqualified keywords for KV encoding types and range operators in transactions and queries:
[:find ?e :where [?e :user/email]]
[:closed "a" "m"]
[:put "users" "u1" {:name "Ada"} :string :data]
[:closed 10 20]
[:string :instant]
Here :string, :data, and :instant are KV type descriptors, while :closed is a range operator. These are intentionally separate from Datalog schema value types such as :db.type/string, which appear under :db/valueType.
Do not quote keywords as strings in Clojure examples. These are different values:
:user/email ;; keyword
"user/email" ;; string
Datalevin's JSON and non-Clojure APIs sometimes encode keywords as strings at the language boundary, but the logical value is still a keyword.
4. Symbols and Datalog Variables
Symbols are names without a leading colon:
?e
?name
parent
fulltext
In Datalog queries, symbols beginning with ? are variables. Datalevin binds them by matching clauses:
[:find ?name
:where [?e :user/email "ada@example.com"]
[?e :user/name ?name]]
The same symbol means the same logical variable within a query. In the example above, both clauses use ?e, so they must match the same entity.
Symbols without ? often name rules or functions:
[(active? ?e)]
[(> ?age 18)]
[(fulltext $ "error budget") [[?e ?a ?v]]]
In Clojure source code, query forms are commonly quoted:
(d/q '[:find ?name
:where [?e :user/name ?name]]
db)
The leading quote tells Clojure to pass the vector as data instead of trying to evaluate symbols such as ?name. In Java, Python, JavaScript, and other client languages, the same query is commonly passed as an EDN string or constructed with an interop helper.
5. Vectors, Lists, Maps, and Sets
Datalevin uses EDN collection forms consistently.
5.1 Vectors
Vectors preserve order and are written with square brackets. Datalevin uses vectors for Datalog queries, datom transactions, lookup refs, idoc paths, tuple values, and range specifications:
[:find ?e :where [?e :user/email]]
[:db/add 42 :user/name "Ada"]
[:user/email "ada@example.com"]
[:profile :address :city]
["acct-42" #inst "2026-06-09T00:00:00Z"]
[:closed 10 20]
When a vector appears inside a Datalog query, position matters. In a data pattern such as [?e :user/name ?name], the positions are entity, attribute, and value.
5.2 Lists
Lists are written with parentheses. In Datalevin query and rule syntax, lists usually represent rule heads, rule calls, or function/predicate calls:
(parent ?child ?parent)
[(parent ?child ?parent)
[?child :person/parent ?parent]]
[(> ?age 18)]
Outside quoted data, a Clojure list means a function call. Inside quoted query or rule data, it is only EDN data until Datalevin interprets it.
5.3 Maps
Maps are written with curly braces and contain alternating keys and values. Datalevin uses maps for schema definitions, entity transactions, option maps, and idoc documents:
{:user/email {:db/valueType :db.type/string
:db/unique :db.unique/identity}
:user/name {:db/valueType :db.type/string}}
{:user/email "ada@example.com"
:user/name "Ada Lovelace"}
{:limit 10
:offset 20}
Map order is not part of the meaning. Examples often align map values vertically to make related fields easier to compare.
5.4 Sets
Sets are written with #{...} and represent unordered unique values:
#{:admin :editor}
A plain {...} form is a map, not a set. Use #{...} when the value is a collection of unique elements without associated values.
6. Tagged Literals
EDN can attach a tag to the next value. Datalevin examples most often use the built-in #inst and #uuid tags.
Do not use custom tagged literals unless the reader or API path you are using has installed support for those tags. In particular, Datalevin reads EDN idoc string payloads with the standard EDN reader path; #inst and #uuid are the safe built-ins to rely on there.
#inst represents an instant in time:
#inst "2026-06-09T12:30:00Z"
#uuid represents a UUID:
#uuid "00000000-0000-0000-0000-000000000001"
These are not strings, even though their textual form contains quoted strings. The tag tells the EDN reader to produce a time or UUID value. This matters for typed schema attributes:
{:event/id {:db/valueType :db.type/uuid
:db/unique :db.unique/identity}
:event/created-at {:db/valueType :db.type/instant}}
When using a non-Clojure client, prefer the client library's EDN helper or typed wrapper for keywords, UUIDs, instants, big integers, big decimals, and byte values. If you send raw JSON strings where the database expects typed values, Datalevin may store strings instead of UUID, instant, or keyword values.
7. EDN in Datalevin
The same EDN forms recur across Datalevin's API surface.
7.1 Schemas
A schema is a map from attribute keywords to property maps:
{:user/email {:db/valueType :db.type/string
:db/unique :db.unique/identity}
:user/age {:db/valueType :db.type/long}
:user/tags {:db/valueType :db.type/keyword
:db/cardinality :db.cardinality/many}}
The outer keys are application attributes. The inner keys are Datalevin schema properties. Most schema property values are also keywords.
7.2 Entity Map Transactions
Entity transactions are maps whose keys are attributes:
{:user/email "ada@example.com"
:user/name "Ada"
:user/tags #{:admin :researcher}}
With a unique identity attribute, the same map shape can insert a new entity or update the existing entity with that identity.
7.3 Datom Transactions
Datom transactions use vectors:
[:db/add 42 :user/name "Ada"]
[:db/retract 42 :user/old-email "ada@old.example"]
[:db/retractEntity 42]
The vector shape is compact and positional, so it is useful when you need to be explicit about the entity id and transaction operation.
7.4 Lookup Refs
A lookup ref is a two-element vector containing a unique attribute and a value:
[:user/email "ada@example.com"]
[:task/id #uuid "00000000-0000-0000-0000-000000000501"]
Lookup refs let transactions and queries refer to entities by domain identity instead of by numeric entity id.
7.5 Datalog Queries
A Datalog query is usually written as an EDN vector containing clauses and keywords such as :find, :where, :in, :keys, and :limit:
[:find ?e ?name
:where [?e :user/email ?email]
[?e :user/name ?name]
:in $ ?email]
Datalevin also accepts the equivalent map form, which is convenient when code or configuration wants named query parts:
{:find [?e ?name]
:in [$ ?email]
:where [[?e :user/email ?email]
[?e :user/name ?name]]
:limit 10}
In Clojure source code, the same map is commonly written with quoted vectors so Clojure passes the query variables as data, while ordinary Clojure expressions such as a local limit value are evaluated before Datalevin receives the map.
{:find '[?e ?name]
:in '[$ ?email]
:where '[[?e :user/email ?email]
[?e :user/name ?name]]
:limit limit}
The query is data. Datalevin interprets that data as a logical query.
7.6 Rules
Rules are EDN data too. A rule set is a vector of rule definitions:
[[(active? ?e)
[?e :user/status :user.status/active]]
[(can-login? ?e)
(active? ?e)
[?e :user/email]]]
Because rule syntax contains lists and symbols, Clojure code usually quotes the rule set, while non-Clojure clients usually pass it through an EDN reader helper.
7.7 Indexed Documents
For :db.type/idoc, EDN can be the document format:
{:doc/id "profile-1"
:doc/content {:profile {:name "Ada"
:age 37
:tags [:math :computing]}}}
When :db/idocFormat is :edn, string payloads are read as EDN and must produce a map. Important document rules:
- The top-level idoc value must be a map.
- Keys may be keywords or strings, depending on the format and client language.
- Lists are not allowed; use vectors for arrays.
- Vectors are also the path collection shape used by idoc path operations.
- Sets are EDN values, but they are not array-like path collections; use vectors when the document shape should behave like an array.
nilvalues are normalized to:json/null.- The literal value
:json/nullis reserved and rejected on input.
Within those rules, EDN idocs can use keyword keys, string keys, vectors, numbers, strings, booleans, instants, UUIDs, sets, and nested maps.
7.8 Dumps and Configuration Files
Datalevin command-line dump/load workflows can use EDN files. EDN is useful for this because it preserves Datalevin-specific values such as keywords, UUIDs, instants, and sets without inventing JSON conventions for each type.
8. EDN and JSON
EDN and JSON are both textual data formats, but they make different trade-offs.
| Feature | EDN | JSON |
|---|---|---|
| Map keys | Any EDN value, commonly keywords | Strings only |
| Symbolic names | Keywords and symbols | Usually strings |
| Sets | Built in with #{...} |
Usually arrays by convention |
| Instants and UUIDs | Tagged literals | Usually strings by convention |
| Comments | Semicolon line comments | Not part of JSON |
| Commas | Optional whitespace | Required separators |
| Top-level value | Any single EDN value | Any JSON value |
This is why this guide's Clojure examples can express Datalevin data directly:
{:task/id #uuid "00000000-0000-0000-0000-000000000501"
:task/status :task.status/running
:task/tags #{:agent :memory}
:task/started #inst "2026-06-09T12:00:00Z"}
The equivalent JSON representation needs conventions for keyword, set, UUID, and instant values. Datalevin's non-Clojure clients provide helpers for those cases.
9. Reading and Printing EDN Safely
In Clojure, use clojure.edn/read-string to read EDN text:
(require '[clojure.edn :as edn])
(edn/read-string "{:user/name \"Ada\"}")
Do not use the general Clojure code reader for untrusted input. EDN is designed as a data reader; the general Clojure reader reads a larger language.
Use pr-str to print ordinary Clojure values in a readable form:
(pr-str {:user/name "Ada" :user/active? true})
;; => "{:user/name \"Ada\", :user/active? true}"
For Datalevin APIs, prefer passing native values when you are already in Clojure. Use EDN strings mainly at language boundaries, in configuration files, or when an API explicitly asks for an EDN form.
10. Common Mistakes
Do not write JSON object syntax when the example expects EDN:
;; EDN
{:user/email "ada@example.com"}
;; JSON, not EDN
{"user/email": "ada@example.com"}
Do not use strings when Datalevin expects keywords:
;; Keyword attribute
:user/email
;; String
"user/email"
Do not forget # when writing a set:
#{:read :write} ;; set
{:read :write} ;; map from :read to :write
Do not forget to quote Datalog queries in Clojure source code:
;; Correct: pass query data.
(d/q '[:find ?e :where [?e :user/email]] db)
;; Incorrect: Clojure tries to evaluate ?e and other symbols.
(d/q [:find ?e :where [?e :user/email]] db)
Do not use null; EDN uses nil.
For Datalevin attributes, do not transact nil as the value. Use omission for unknown values and retraction for values that should no longer be present. The exception is nested idoc content, where nil normalizes to :json/null.
Do not treat tagged literals as plain strings:
#uuid "00000000-0000-0000-0000-000000000001" ;; UUID value
"00000000-0000-0000-0000-000000000001" ;; string value
11. Quick Reference
| Need | Use |
|---|---|
| Attribute name | :user/email |
| Enum value | :order.status/paid |
| Datalog variable | ?e, ?name, ?score |
| Entity transaction | {:user/email "ada@example.com"} |
| Datom transaction | [:db/add 42 :user/name "Ada"] |
| Lookup ref | [:user/email "ada@example.com"] |
| Query | [:find ?e :where [?e :user/email]] |
| Rule set | [[(rule ?x) [?x :attr/value]]] |
| Set | #{:a :b :c} |
| Instant | #inst "2026-06-09T12:00:00Z" |
| UUID | #uuid "00000000-0000-0000-0000-000000000001" |
| Missing value | nil |
EDN's main advantage in Datalevin is that the examples show the actual data structures the database consumes. Once the literal forms are familiar, schemas, transactions, queries, rules, and indexed documents all read as variations on the same small notation.
References
[1] Rich Hickey and contributors, "Extensible Data Notation," official edn-format specification. URL: https://github.com/edn-format/edn.
User Examples
Log in to create examplesNo examples for this chapter yet.
