Skip to content

Agent documents

The Elucim DSL is the agent-friendly document layer for host apps that want generated diagrams to be easy to patch, refine, and preview. The canonical document shape is normalized by stable element ID and animated with explicit timelines and state machines.

Elucim documents are normalized by stable element ID:

import type { ElucimDocument } from '@elucim/dsl';
const doc: ElucimDocument = {
version: '2.0',
scene: {
type: 'player',
width: 1920,
height: 1080,
children: ['headline', 'metric'],
},
elements: {
headline: {
id: 'headline',
type: 'text',
role: 'title',
layout: { x: 120, y: 120 },
props: { type: 'text', content: 'CutReady impact', fill: '$title' },
},
metric: {
id: 'metric',
type: 'text',
role: 'metric',
layout: { x: 120, y: 260 },
props: { type: 'text', content: '42% faster', fill: '$accent' },
},
},
};

Agents should make stable IDs part of their output contract. That lets a host app ask for precise edits like “update metric” instead of regenerating the whole slide.

Use @elucim/dsl/agent when a host app or LLM wants high-impact edits without memorizing every schema detail. The helpers are deterministic and pure: they return a new document plus summaries and validation feedback.

import {
applyAgentCommands,
createDocument,
evaluateSceneForAgent,
} from '@elucim/dsl/agent';
const doc = applyAgentCommands(createDocument({
preset: 'slide',
metadata: {
title: 'Derivative intuition',
intent: 'Explain slope as local rate of change.',
},
}), [
{
op: 'addElement',
element: {
id: 'headline',
type: 'text',
role: 'title',
intent: { role: 'title', description: 'Introduce the takeaway', importance: 'primary' },
layout: { x: 120, y: 120 },
props: { content: 'Slope is local change', fill: '$title' },
},
},
{ op: 'addRevealTimeline', timeline: { id: 'intro', targets: ['headline'], preset: 'fadeIn' } },
{ op: 'createStateMachine', stateMachine: { id: 'main', timelineId: 'intro', start: 'onStart' } },
]).document;
const report = evaluateSceneForAgent(doc);

The generated motion is explicit timeline/state-machine data. The agent helpers do not create wrapper animation props, which keeps generated scenes aligned with the normalized state-machine model.

Use agent validation at host boundaries:

import { validateForAgent } from '@elucim/dsl/agent';
const validation = validateForAgent(doc);
if (!validation.valid) throw new Error(validation.errors.map(e => `${e.path}: ${e.message}`).join('\n'));

The visual editor opens normalized documents as the native authoring path. It preserves metadata, element intent, timelines, and state machines where possible, and exposes timeline, state-machine, metadata, intent, and nudge review controls.

Host apps and agents can use applyAgentCommands() for common edits and applyCommand() for lower-level deterministic operations:

import { applyAgentCommands } from '@elucim/dsl/agent';
const updated = applyAgentCommands(doc, [
{
op: 'updateElement',
id: 'metric',
patch: {
props: { content: '51% faster' },
layout: { x: 160, y: 280 },
},
},
]).document;

Commands are pure: the input document is not mutated. Agent commands cover adding, updating, deleting, moving, reparenting, sibling reordering, timeline creation, reveal presets, state-machine creation, metadata updates, and deterministic nudges. Use scene.children and each group element’s children array as the stacking order; later siblings paint on top, so agents should call reorderElement(), bringElementForward(), or sendElementBackward() instead of adding z-index metadata.

Use the document services to give agents compact context and repairable feedback:

import { diffDocuments, summarizeDocument, validateForAgent } from '@elucim/dsl/agent';
const summary = summarizeDocument(doc);
const validation = validateForAgent(doc);
const patch = diffDocuments(before, after);

validateForAgent() returns structured validation errors plus repair hints. diffDocuments() returns JSON-patch-shaped operations that are useful for review, logging, and host approval flows.

Agents can also ask Elucim to inspect timeline bounds, repair common generated mistakes, and prove that a timeline changes visible properties over sampled frames:

import {
createLoopingStateMachine,
getTimelineBounds,
inspectSceneForAgent,
repairDocumentForAgent,
sampleAnimationForAgent,
} from '@elucim/dsl/agent';
const bounds = getTimelineBounds(doc);
const repaired = repairDocumentForAgent(doc);
const animation = sampleAnimationForAgent(repaired.document, 'intro');
const inspection = inspectSceneForAgent(repaired.document, { timelineId: 'intro' });
const playable = createLoopingStateMachine(repaired.document, { timelineId: 'intro' }).document;

repairDocumentForAgent() is intentionally conservative: today it extends timelines whose keyframes exceed their declared duration and returns summaries, validation, and a JSON-patch diff. sampleAnimationForAgent() evaluates selected frames and reports which element properties changed, which gives agents a cheap way to answer “does this scene actually animate?” before launching a browser.

inspectSceneForAgent() is the higher-level “can I see it?” check. It samples frames, estimates element bounds, reports visible element counts and occupied area, and flags common weak-scene issues such as blank frames, tiny compositions, off-canvas elements, zero-size elements, low literal color contrast, and timelines that do not visibly change.

Timelines are keyframe clips over a small safe property set: opacity, translate, scale, rotate, fill, and stroke.

import { applyTimelineFrame, transitionStateMachine } from '@elucim/dsl';
const previewDoc = applyTimelineFrame(doc, 'intro', 18);
const nextState = transitionStateMachine(doc, 'deck', 'idle', 'start');

State machines use a Rive-inspired but intentionally small model. Every machine has a visual Entry node, entry matches the explicit Entry transition target, states point at timelines, and machine-level transitions connect states. The Entry transition declares how the machine leaves Entry: onStart fires automatically when a preview/run begins, while events like onClick or onKey can intentionally gate the first state. A trigger transition fires when its trigger event is activated; an exitTime transition is the automatic Next path after the source state’s timeline finishes. A transition whose target is entry resolves through Entry again, while a transition whose target is exit reaches the terminal Exit pseudo-node and stops that machine.

The minimal event presets are onStart, onClick, onKey, and reset. Use onKey plus key metadata for keyboard interactions such as Enter, Space, Escape, or ArrowRight; key matching is case-insensitive, so G and g are the same binding. reset is still a normal event transition; the convention is to target entry, then let Entry decide whether to restart immediately (onStart) or wait for its gated start event. Use a custom trigger name for domain events the host app sends, such as back, timeout, answerCorrect, or dataLoaded; custom names have no special behavior beyond matching the event string.

stateMachines: {
deck: {
id: 'deck',
entry: 'idle',
inputs: {
start: { type: 'trigger' },
reset: { type: 'trigger' },
},
states: {
idle: { timeline: 'intro' },
focus: { timeline: 'focus' },
},
transitions: [
{ id: 'entry-start', from: 'entry', to: 'idle', trigger: 'onStart' },
{ id: 'idle-start', from: 'idle', to: 'focus', trigger: 'start' },
{ id: 'focus-reset', from: 'focus', to: 'entry', trigger: 'reset' },
{ id: 'focus-next', from: 'focus', to: 'exit', exitTime: 1 },
],
},
}

Scenes do not own duration. Timeline durations are scoped to the state being previewed; during state-machine playback, the active state’s timeline duration controls when Next auto-runs. Fixed-frame output must use an explicit export policy such as a selected timeline, selected state, scripted machine path, or machine run with a max-frame cap.

State-machine visual previews are composed as an ordered frame stack: all timelines are first applied at their start frame, then completed states and the active state overlay later in path order. If a timeline appears more than once in that stack, the later frame wins. Generic DSL viewers that render a document through the React DslRenderer run the default state machine with the same click, key, Next, Entry, Exit, and finished-frame semantics used by the editor. Static compatibility and export paths still apply the default state machine’s initial frame stack so pasted JSON/YAML opens at the authored start state even outside an interactive runtime.

Editor graph positions are optional metadata under layout: layout.entry stores the visual Entry node, layout.states stores state node positions, and layout.viewport stores the graph pan/zoom. Multiple state machines are independent interaction controllers over the same scene/timelines; a host chooses which machine is active for a given surface or interaction mode.

Elucim does not call an LLM. Host apps such as CutReady can ask Elucim for deterministic nudges, show them to an agent or human, and apply the exact command list:

import { analyzePolish, applyNudge, suggestDocumentNudges } from '@elucim/dsl';
const nudges = suggestDocumentNudges(doc);
const safeNudges = nudges.filter(nudge => nudge.confidence === 'safe');
const polished = safeNudges.reduce((current, nudge) => applyNudge(current, nudge).document, doc);
const polish = analyzePolish(polished);

Polish analysis returns category scores and diagnostics for layout, hierarchy, readability, contrast, graph readability, explanatory structure, and motion. Current nudges cover safe metadata/readability/title hierarchy polish, a reviewable timeline-based intro clip, reviewable layered graph layout for graph elements, semantic ELK layout for relationship-rich ordinary elements, and smoother connector continuations. This gives agents a simple workflow: generate the 80% layout, ask Elucim for refinements, apply safe command-backed nudges, and explain review nudges that should be confirmed by a human or opened in the editor.

For broader explanatory scenes, agents should author explicit semantic relationships such as intent.target, intent.flowFrom, intent.flowTo, intent.relationship, and layout.rank. Hosts can then call suggestSemanticLayoutNudges(doc) to get an ELK-backed review nudge that arranges ordinary elements from those relationships and writes the result back as normal document coordinates. Use layout.locked: true for elements that should not move. Agents can call inspectPolishHeuristics(doc) to interrogate the raw layout and quality evidence, including element intersections, graph crossings, literal colors, text sizing, overflow, connector continuations, and semantic relationships.

Hosts that expose Elucim tools to agents can call getAgentOperationCatalog() from @elucim/dsl/agent and pass those operation descriptors alongside the document so the agent can plan against the real code-backed ops instead of prose-only instructions.

Use composite helpers when an agent needs polished explanatory structure but the user still needs editable pieces in the editor. These helpers emit normal Elucim groups/primitives and optional timelines:

import {
createCardGridPreset,
createConnectorPreset,
createDecisionNodePreset,
createProgressiveRevealGroupPreset,
createTimelineRoadmapPreset,
} from '@elucim/dsl';
const grid = createCardGridPreset({
id: 'pipeline',
x: 80,
y: 140,
columns: 3,
items: [
{ id: 'draft', title: 'Draft', body: 'Generate a structured first pass.' },
{ id: 'review', title: 'Review', body: 'Inspect layout and wording.' },
{ id: 'publish', title: 'Publish', body: 'Ship a polished document.' },
],
});
const connector = createConnectorPreset({
id: 'draft-to-review',
from: { id: 'draft', bounds: { x: 80, y: 140, width: 260, height: 150 } },
to: { id: 'review', bounds: { x: 372, y: 140, width: 260, height: 150 } },
label: 'then',
lineStyle: 'dashed',
startCap: 'dot',
endCap: 'arrow',
});

createConnectorPreset() writes intent.flowFrom, intent.flowTo, and intent.relationship, so semantic layout can use connectors as virtual ELK edges while keeping the persisted document editable. Straight line and smooth bezierCurve connectors both support lineStyle, startCap, endCap, strokeLinecap, and strokeLinejoin.

For shell workflows, use the CLI shortcuts for the most common composites:

Terminal window
npx @elucim/cli add-card-grid diagram.elc --id pipeline --x 80 --y 140 --items-json '[{"id":"draft","title":"Draft","body":"Generate"},{"id":"review","title":"Review","body":"Inspect"}]' --out diagram.elc --json
npx @elucim/cli add-connector diagram.elc --id draft-to-review --from draft --to review --line-style dashed --start-cap dot --end-cap arrow --out diagram.elc --json

Agents should describe motion in beats and semantic verbs, then let Elucim compile those intents to timelines/state machines. This is safer than asking a model to hand-compute every keyframe.

import {
createAutoStaggerTimeline,
createReducedMotionDocument,
createSemanticMotionTimeline,
createStateSnapshotMotion,
holdFinalFrame,
lintMotion,
planMotionBeats,
previewBeatDiffs,
} from '@elucim/dsl';
const beats = planMotionBeats({ seconds: 12, fps: 30, beatCount: 4 });
const reveal = createSemanticMotionTimeline(doc, {
id: 'intro-flow',
preset: 'revealFlow',
targets: ['draft', 'review', 'publish'],
duration: beats[0].duration,
});
const handoff = createSemanticMotionTimeline(doc, {
id: 'draft-to-review',
preset: 'handoff',
from: 'draft',
to: 'review',
connectorId: 'draft-to-review',
});
const stagger = createAutoStaggerTimeline(doc, {
id: 'ranked-reveal',
group: 'pipeline',
orderBy: 'rank',
});
const animated = { ...doc, timelines: { [reveal.id]: reveal, [handoff.id]: handoff, [stagger.id]: stagger } };
const lint = lintMotion(animated, { requireReducedMotion: true });
const diffs = previewBeatDiffs(animated, { timelineId: reveal.id, beats });
const fallback = createReducedMotionDocument(animated, { mode: 'minimal' });
const poster = holdFinalFrame(animated, { timelineId: reveal.id });

Use revealFlow, emphasizeDecision, tracePath, loopOnce, handoff, drainQueue, and compareBeforeAfter as the first motion vocabulary. createStateSnapshotMotion() turns named states into timelines plus a state machine, and holdFinalFrame() creates static poster/final-state documents. lintMotion() flags blank first frames, too-fast transitions, simultaneous overload, hidden labels, flashing, excessive motion, static timelines, and missing reduced-motion fallbacks. previewBeatDiffs() gives agents a before/after summary of what appears, disappears, moves, or changes at each beat.

The CLI mirrors the same workflow:

Terminal window
npx @elucim/cli add-beat diagram.elc --id intro-flow --preset revealFlow --targets draft,review,publish --out diagram.elc --json
npx @elucim/cli animate-flow diagram.elc --id draft-handoff --from draft --to review --connector draft-to-review --out diagram.elc --json
npx @elucim/cli sample-beats diagram.elc --timeline intro-flow --beats 4 --json
npx @elucim/cli hold-final diagram.elc --timeline intro-flow --out poster.elc --json
npx @elucim/cli reduced-motion diagram.elc --mode minimal --out reduced.elc --json