Status: Experimental. The import leg of the Grain adapter (roadmap item 8, executed 2026-07-10). Importer:
import-catalog!inatlas.adapter.grain(examples/grain/src/atlas/adapter/grain.clj). Target: grain-todo-list — a real grain application we did not author (two services, auth, sessions, Datastar UI).
Where the counter demo shows Atlas authoring a grain app from scratch, the import leg is the adoption path: take an existing grain application and lift it into the Atlas registry — data-flow, invariants, and cloud tooling included — without touching its source.
Phase 1 — capture (runs in the app's own JVM; needs nothing but grain): a short script requires the service namespaces (populating grain's global registries at load time), then writes a plain-EDN capture:
:authorized? reduced to a
presence flag{:file :line} from var metadatadefschemas var names
(event-schemas, command-schemas, …) — no guessing which schema is an
eventPhase 2 — import (runs on the atlas side): import-catalog! turns the
capture into registered atlas entities and returns a report.
Three tiers, kept strictly apart:
:domain/<ns> from the grain name; :effect/write|read from
kind; :access/enforced from :authorized? presence; declared consumes
(read models' :events, processors' :topics); :version, schedules,
schemas.:operation/* / :entity/*
aspects are guessed — inventing semantics from name-tokenizing would
pollute the vocabulary. Every imported entity carries a :draft/<name>
aspect instead: compound-ids stay mechanically unique, and the aspect
doubles as a work queue for human/LLM enrichment via suggest-placement
and refactor-aspect.:import/scanned-dataflow true.73 entities: 28 events, 27 commands, 12 queries, 4 read models,
2 processors; 33 with scan-recovered data-flow. Snapshotted as
grain/todo-list/main/v0.1.0 (14 fn-free meta entities + 73 app entities).
All error-level invariants pass — every command and query has a real
:authorized? predicate. A properly gated app, in instructive contrast to
grain's own example-service where every command is (constantly true).
One real finding: :event.user/logged-in is an orphan — produced
by :command.user/login, consumed by nothing. Two independent invariant
engines converge on it: the grain ontology's grain-orphan-events and
core's pre-existing internal-fn-outputs-consumed.
Data-flow is answerable through the standard cloud tool surface:
consumers-of :event.todo/task-captured → :read-model.todo/tasks
producers-of :event.user/logged-in → :command.user/login
consumers-of :event.user/logged-in → (none)
These cost a debugging session each; they are now behavior, not lore.
1. The trace tools read data keys, not type-refs.
consumers-of / producers-of / trace-data-flow build their index from
:execution-function/context and :execution-function/response
(atlas.ide.trace), not from the datalog edges that type-refs produce. The
fix is semantically honest rather than a workaround: events are data, so
the importer mirrors produced events into :execution-function/response
(commands) and consumed events into :execution-function/context (read
models, processors). No key-mixing conflicts arise: payload context exists
only on commands/queries; event-consumption context only on read models and
processors. :grain/produces/:grain/consumes remain the grain-facing
declaration that materialize! projects.
2. Snapshots must carry their ontology descriptors.
Under cloud--version, context-for resolves dataflow keys through the
ontology entry (:ontology/for :atlas/execution-function) found in the
pulled snapshot — if the snapshot holds only app entities, every trace
tool silently returns empty. This is exactly the gap the
tooling audits, and why production registries carry their
:ontology/for entries as data. Exports must
include the fn-free meta entities (ontology descriptors + type-refs)
alongside app entities.
# Phase 1 — in the grain app checkout (grain-todo-list); edit the config
# map in the script for other apps:
clojure -M:dev -e "(load-file \"<atlas>/examples/grain/scripts/capture.clj\")"
# Phase 2 — in examples/grain:
clojure -M -e "
(require '[atlas.adapter.grain :as adapter] 'atlas.ontology.grain
'atlas.ontology.execution-function 'atlas.ontology.data-schema)
(adapter/import-catalog! (read-string (slurp \"capture.edn\")))
(require '[atlas.invariant :as inv]) (inv/report)"
:draft/* enrichment is the designed human/LLM follow-up: pick a domain,
run suggest-placement per entity, replace drafts with real vocabulary.test/app/grain_counter.clj, examples/grain/src/grain_demo/counter.clj).exportable-snapshot fn (H1.3)
so pushes are correct by construction.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |