Lesson 02 — Application & Document Model
Every useful Affinity script begins the same way: get the current document, inspect it, and decide what to do with it. This lesson covers the full document model — from the app singleton at the top, down through spreads, pages, and properties like DPI, format, and title.
Before you can move a node, change a colour, or export a file, you need a valid document reference. New developers hit a few key pitfalls: the difference between Document.current and Document.all, the spread/page distinction, and the selection-vs-spread ordering rule. Mastering these makes your scripts behave predictably across documents.
What this lesson covers
- The
appsingleton as entry point Document.currentvsDocument.all- Document properties: title, path, format, DPI, dimensions, size and page count
- The spread/page distinction
- Sync vs async document operations (async means the operation runs in the background and returns a result later)
- The critical selection-vs-spread ordering rule
- Document history and snapshots
- Undo/redo and selection convenience methods
- Guide management and drawing scale
The document hierarchy — JS API vs node tree
It is important to distinguish between the JavaScript API objects and the actual node tree that lives inside Affinity.
JS API objects:
app
│
Document (doc)
├── rootNode
├── currentSpread
├── selection
└── history
Node tree (inside Affinity):
DocumentNode (parent: null)
└── SpreadNode(s)
└── Layers / children
Mapping:
doc.rootNode → DocumentNode
doc.currentSpread → SpreadNode (child of DocumentNode)
(The Document object itself is not a node — doc.isDocumentNode is undefined.)
The Document object (doc) is the JS API entry point — it holds properties like currentSpread, selection, history, and rootNode, but it is not itself a node (doc.isDocumentNode returns undefined).
The node tree starts at rootNode, a DocumentNode (rootNode.isDocumentNode === true). It has parent: null — it is the true root. Every node in the document, no matter how deeply nested, is a descendant of this DocumentNode. It has exactly one type of direct child: SpreadNode objects — one per spread in the document.
That is essentially all it exposes. It has no fill, no transform, no visibility — it is purely a container.
The root node exists to give tree traversal a consistent entry point. Every node, including spreads themselves, is a child of something. The DocumentNode is the answer to "what is the parent of a spread?" — without it, the tree would have no single root and recursive walkers would need special-case handling for the top level.
// Snippet
const rootNode = doc.rootNode;
rootNode.spreadCount // same as doc.spreadCount
rootNode.pageCount // same as doc.pageCount
rootNode.isDocumentNode // true
In practice you almost never use it directly. The SDK gives you more convenient access points that you will use instead. The only time you need doc.rootNode directly is when you want to write a recursive walker that starts from the absolute top of the tree and descends through every spread and every layer in one pass — the kind of thing lesson 03 covers.
Getting the current document
'use strict';
const { Document } = require('/document.js');
const doc = Document.current;
if (!doc) {
console.log('No document open.');
} else {
console.log('Title: ', doc.title);
console.log('Path: ', doc.path);
console.log('Format: ', doc.format);
console.log('DPI: ', doc.dpi);
console.log('Width (px): ', doc.widthPixels);
console.log('Height (px): ', doc.heightPixels);
console.log('Spreads: ', doc.spreadCount);
console.log('Pages: ', doc.pageCount);
console.log('Needs save: ', doc.needsSaving);
console.log('Read only: ', doc.isReadOnly);
}
Use
.titlenot.name—.nameis undefined on Document objects. The SDK test suite uses.title.
All open documents
Use Document.all to get an array of every document currently open. This is useful for batch operations or multi-document workflows.
'use strict';
const { Document } = require('/document.js');
const docs = Document.all;
console.log(`${docs.length} document(s) open`);
for (const d of docs) {
console.log(d.title, '—', d.path);
}
Spreads vs pages
- A spread is a physical canvas unit. In most documents, one spread = one page.
- In a facing-pages publication, one spread = two pages side by side.
doc.spreads— iterable collection ofSpreadNodeobjectsdoc.pageCount— total number of pages across all spreadsdoc.spreadCount— number of spreads
'use strict';
const { Document } = require('/document.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
for (const spread of doc.spreads) {
console.log('Spread:', spread);
}
// Shortcuts
const firstSpread = doc.spreads.first;
const currentSpread = doc.currentSpread;
}
The selection / currentSpread ordering rule
This is the most important gotcha in the entire SDK.
// Snippet
// WRONG — selection is lost before you read it
doc.executeCommand(DocumentCommand.createSetCurrentSpread(spread));
const sel = doc.selection; // always empty!
// CORRECT — capture selection BEFORE changing spread
const sel = doc.selection;
const nodes = sel.nodes; // extract everything you need
doc.executeCommand(DocumentCommand.createSetCurrentSpread(spread));
setCurrentSpread clears the selection. Always read doc.selection and extract all needed data before executing that command.
Document state flags
| Property | Meaning |
|---|---|
doc.needsSaving | Has unsaved changes |
doc.isDirty | Same as needsSaving |
doc.isReadOnly | Cannot be edited |
doc.mustSaveAs | New doc — must be saved to a path first |
doc.isEmbedded | Placed inside another document |
doc.insertionMode | Current insertion behaviour (InsertionMode, read-only) |
Creating a new document
'use strict';
const { Document, NewDocumentOptions } = require('/document.js');
const opts = NewDocumentOptions.createDefault();
opts.width = 1920;
opts.height = 1080;
opts.dpi = 96;
const newDoc = Document.create(opts);
console.log('Created:', newDoc.title);
Beyond width/height/dpi: opts.isFacing, opts.isVerticalStack, opts.isDoublePageStart control facing-pages layout; opts.createArtboard, opts.isTransparentBackground, opts.isMultiPage, opts.createMaster control artboard mode, transparency, multi-page, and master pages; opts.marginsEnabled, opts.viewDpi, opts.imagePlacement, opts.colourProfile, opts.rasterFormat, opts.bleed refine the setup. Validate sizes up front with NewDocumentOptions.getMaxDpi(unit, dimension) and NewDocumentOptions.getMaxSize(unit, dpi).
Async document operations
For long-running work, use doc.promises:
'use strict';
const { DocumentPromises } = require('/document.js');
const { app } = require('/application.js');
async function main() {
const doc = await DocumentPromises.getCurrent();
if (!doc) return;
// save the file on the desktop
const desktop = app.userDesktopPath;
await doc.saveAs(`${desktop}/output.af`);
console.log('Saved.');
}
main();
Closing documents
Scripts can close a document when a batch or automated workflow has finished. Three forms are available — synchronous, callback-based, and Promise-based:
'use strict';
const { Document } = require('/document.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
doc.save();
doc.close();
console.log('Closed.');
}
Closing presents no save prompt, so save explicitly first if changes need preserving. After a successful close the Document handle is no longer valid. For end-to-end automation, close in a finally block so the document never stays open behind a failure:
// Snippet — assumes sourcePath and exportOptions are defined
const doc = Document.load(sourcePath);
try {
doc.export(outputPath, exportOptions);
}
finally {
doc.close();
}
Document properties
The DocumentPreset class lists all built-in templates. NewDocumentOptions lets you customise every detail of a new document, including its drawing scale.
'use strict';
const { Document, DocumentPreset, NewDocumentOptions } = require('/document.js');
const { DrawingScale } = require('/drawingscale.js');
// Enumerate all built-in document presets
const presets = DocumentPreset.all;
console.log(`${presets.length} presets available`);
for (const p of presets.slice(0, 5)) {
console.log(` ${p.name} — ${p.width}×${p.height} @ ${p.dpi}dpi`);
}
// Create a document from a preset
const a4Preset = presets.find(p => p.name === 'A4');
if (a4Preset) {
const doc = Document.createFromPreset(a4Preset, false); // false = portrait
console.log('Created:', doc.title);
}
// Create with full options including drawing scale
const opts = NewDocumentOptions.createDefault();
opts.width = 297; // mm equivalent in document units
opts.height = 210; // A4 landscape
opts.dpi = 300;
opts.useDrawingScale = true;
opts.drawingScale = DrawingScale.createFromIntegers(1, 50, true); // 1:50 scale
'use strict';
const { Document } = require('/document.js');
// doc.getFontNames() — all fonts referenced in the document
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
const names = doc.getFontNames();
console.log(`Document uses ${names.length} font(s):`);
for (const name of names) console.log(' ', name);
}
Drawing scale
Affinity supports architectural and engineering drawing scales (e.g. 1:50, 1:100). Create them with createFromIntegers, or fetch the built-in defaults for a given unit system.
'use strict';
const { DrawingScale } = require('/drawingscale.js');
// Common scales — createFromIntegers(left, right, simplifyFraction)
const scale = DrawingScale.createFromIntegers(1, 100, true); // 1:100
console.log(scale.toString(false, true, false)); // "1 : 100"
console.log('Left:', scale.leftValue, 'Right:', scale.rightValue);
// Identity (no scale, 1:1)
const identity = DrawingScale.identity;
// Available defaults for a unit — pass a raw integer, NOT UnitType.*
// UnitType.Metres etc. return undefined from JS and cause INVALID_ARGS.
// Use these verified integer constants instead:
const DrawingScaleUnit = {
Pixels: 5, // screen/pixel defaults
Mixed: 6, // pixels + imperial
Points: 7, // typographic points
Millimetres: 8, // mm (includes km map scales)
Centimetres: 9, // cm (includes km map scales)
Picas: 10, // picas
Inches: 11, // inches (includes ft)
Yards: 12, // yards
Feet: 13, // feet
Kilometres: 14, // kilometres
Metres: 15, // metres
};
const mmDefaults = DrawingScale.getDefaults(DrawingScaleUnit.Millimetres);
console.log(`${mmDefaults.length} mm presets:`);
for (const s of mmDefaults) {
console.log(' ', s.toString(false, true, false));
}
// Output:
// 1 : 10
// 1 : 25
// 1 : 40
// 1 : 50
// 1 : 100
// 1 : 200
// 1 : 500
// 1 mm : 1 cm
// 1 mm : 1 m
// 1 : 25000 km
// 1 : 50000 km
Do not pass
UnitType.Millimetresor anyUnitType.*value togetDefaults().UnitTypeis a C++ constructor — its named properties returnundefinedfrom JavaScript, which causesINVALID_ARGS. Use the integer constants from theDrawingScaleUnittable above instead.
Document snapshots
Snapshots let you bookmark the document state — useful for before/after comparisons or checkpointing during long scripts. Create, list, and restore them via DocumentCommand.
'use strict';
const { Document } = require('/document.js');
const { DocumentCommand } = require('/commands.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
// Add a named snapshot
doc.executeCommand(DocumentCommand.createAddDocumentSnapshot('Before changes'));
// List snapshots
for (const snap of doc.snapshots) {
console.log(snap.description, snap.format);
}
// Restore the first snapshot
if (doc.snapshots.length > 0) {
doc.executeCommand(DocumentCommand.createRestoreDocumentSnapshot(doc.snapshots[0]));
}
// Which snapshot is active, and where it sits in history
console.log('Current snapshot:', doc.currentSnapshot?.description);
console.log('Snapshot index:', doc.currentSnapshotIndex, 'history index:', doc.currentSnapshotHistoryIndex);
// Restore a snapshot into a brand-new document (keeps the current doc untouched)
const fresh = doc.currentSnapshot.createDocument();
console.log('Restored into:', fresh.title);
}
doc.enumerateSnapshots(cb) walks snapshots with a callback instead of the doc.snapshots array. DocumentSnapshot.createDocumentAsync(cb) is the async variant of createDocument().
Spread & artboard document properties
Use SpreadDocumentProperties and ArtboardDocumentProperties to read and write per-artboard settings like margins, and to resize spreads and artboards relative to an anchor point.
'use strict';
const { Document } = require('/document.js');
const { ArtboardDocumentProperties, SpreadDocumentProperties } = require('/documentproperties.js');
const doc = Document.current;
const spread = doc.currentSpread;
const artboard = spread.artboardInterface;
// Read / write artboard properties
const props = ArtboardDocumentProperties.create()
.setUseMargin(true)
.setMargin({ top: 20, left: 20, bottom: 20, right: 20 });
doc.setArtboardDocumentProperties(artboard, props);
// Resize spread about an anchor
doc.setSpreadSizeWithAnchor(spread, 800, 600, SpatialAnchor.TopLeft);
// Resize artboard about an anchor
doc.setArtboardSizeWithAnchor(artboard, 400, 300, SpatialAnchor.Centre);
Guide management
Guides are non-printing alignment lines — horizontal (true) or vertical (false) — positioned at a pixel coordinate. Add, move, remove, and recolour them through convenience methods on doc.
'use strict';
const { Document } = require('/document.js');
const doc = Document.current;
if (doc) {
// Add guides
doc.addGuide(true, 200); // horizontal guide at y=200px
doc.addGuide(false, 400); // vertical guide at x=400px
// Convenience methods
// doc.addHorizontalGuide(pixels) / doc.addVerticalGuide(pixels)
// doc.moveGuide(horizontal, index, newPixels)
// doc.removeGuide(horizontal, index)
// doc.setGuidesColour(colour)
console.log('Guides added.');
}
Document size and page count
Quick-access properties for common document dimensions. sessionUuid uniquely identifies this document instance within the Affinity session.
// Snippet
const doc = Document.current;
console.log('Width:', doc.widthPixels);
console.log('Height:', doc.heightPixels);
console.log('Page count:', doc.pageCount);
console.log('Spread count:', doc.spreadCount);
console.log('Session UUID:', doc.sessionUuid);
Document history
The doc.history object exposes undo/redo state, and doc.undo() / doc.redo() let you step through it without building a command.
// Snippet
const doc = Document.current;
const h = doc.history;
console.log('Can undo:', h.canUndo, '—', h.undoDescription);
console.log('Can redo:', h.canRedo, '—', h.redoDescription);
console.log('History size:', h.size);
// Undo/redo via document convenience methods
doc.undo();
doc.redo();
Undo and selection convenience methods
The Document class exposes many common operations directly, without needing to build a DocumentCommand manually:
// Snippet
const doc = Document.current;
const sel = doc.selection;
doc.undo(); // undo last action
doc.redo(); // redo
doc.selectAll(); // select all
doc.deleteSelection(sel); // delete selected nodes
doc.showSelection(sel); // make visible
doc.hideSelection(sel); // hide
doc.lockSelection(sel); // lock (non-editable)
doc.unlockSelection(sel); // unlock
doc.setLayerDescription('My layer', sel); // rename layer
doc.convertToCurves(sel); // convert shape to curves
doc.rasteriseObjects(sel); // rasterise vector to pixels
doc.flatten(); // flatten all layers
doc.mergeVisible(); // merge visible layers
// Additional convenience methods
doc.setText('Hello', sel); // replace text
doc.formatText(delta, sel); // apply text formatting
doc.insertGlyph(0x2665, sel); // insert ♥ glyph
doc.setDashPhase(0, sel); // dash phase offset
doc.setBalancedDashes(true, sel); // balanced dashes at corners
// Saving and closing
doc.save(); // save in place (fails if mustSaveAs)
doc.saveAs('/path/to/file.af'); // save to new path
doc.saveAsPackage('/path/to/pkg',
PackageResourcesPolicy.CopyLinks); // package with linked assets
// doc.close(); // close (no save prompt — save first; handle invalid after)
PackageResourcesPolicy controls how linked resources are handled when packaging. Import it from /document:
// Snippet
const { PackageResourcesPolicy } = require('/document.js');
// Values: CopyLinks (copies linked assets into the package)
Document units and page properties
DocumentCommand.createSetDocumentUnits(sel, units) sets the document's measurement units. DocumentCommand.createSetPageDocumentProperties(sel, props) sets page-level properties like page colour and margins. UnitValue.getTypeCategory(unitType) classifies a unit as a distance, angle, relative, data or temperature value.
// Snippet — assumes sel and doc are defined
const { DocumentCommand } = require('/commands.js');
const { UnitType } = require('/units.js');
doc.executeCommand(DocumentCommand.createSetDocumentUnits(sel, UnitType.Millimetre));
Key gotchas
mustSaveAs on new documents — a freshly created document has no path. Calling doc.save() on it will fail. Save to the Desktop with doc.saveAs(desktop + '/file.af').
doc.close() and doc.closeAsync() always throw NOT_IMPLEMENTED. You cannot close a document from a script. If your workflow needs a "save and done" pattern, save the document and let the user close it manually.
setCurrentSpreadclears selection — always read selection data before calling this command.
Exercises
Exercise 2.1 — Document dashboard
Write a script that logs a full summary of the current document: title, path, format, DPI, pixel dimensions, spread count, page count, and whether it needs saving.
Exercise 2.2 — Multi-doc lister
Write a script that lists all open documents and, for each, logs the title and whether it's dirty.
Exercise 2.3 — Spread walker
Iterate every spread in the document. For each spread, log its index and artboard count. (Hint: check spread.artboardCount.)
Exercise 2.4 — New document
Create a new document programmatically (1920×1080, 96 DPI), log its title, then save it to the Desktop as exercise-2-4.af.
SDK modules used in this lesson
/document—Document,NewDocumentOptions,DocumentPreset,DocumentPromises,PackageResourcesPolicy/application—app/drawingscale—DrawingScale/commands—DocumentCommand
See also
Example scripts
02a-document-dashboard.js
Download 02a-document-dashboard.js
'use strict';
// Example 02a — Document dashboard
// Lesson 02: App & Document
//
// Logs a full summary of the current open document.
const { Document } = require('/document.js');
const { app } = require('/application.js');
console.log('=== Application ===');
console.log('Product:', app.productShortName, app.shortVersion);
console.log('Desktop:', app.userDesktopPath);
const doc = Document.current;
if (!doc) {
console.log('\nNo document is open. Open a file and run again.');
} else {
console.log('\n=== Document ===');
console.log('Title: ', doc.title);
console.log('Path: ', doc.path || '(unsaved)');
console.log('Format: ', doc.format);
console.log('DPI: ', doc.dpi);
console.log('View DPI: ', doc.viewdpi);
console.log('Width (px): ', doc.widthPixels);
console.log('Height (px): ', doc.heightPixels);
console.log('Spreads: ', doc.spreadCount);
console.log('Pages: ', doc.pageCount);
console.log('Snapshots: ', doc.snapshotCount);
console.log('Needs save: ', doc.needsSaving);
console.log('Read only: ', doc.isReadOnly);
console.log('Must save as:', doc.mustSaveAs);
console.log('Embedded: ', doc.isEmbedded);
console.log('UUID: ', doc.persistentUuid);
}
02b-spread-walker.js
'use strict';
// Example 02b — Spread walker
// Lesson 02: App & Document
//
// Iterates every spread and logs its properties.
const { Document } = require('/document.js');
const doc = Document.current;
if (!doc) { console.log('No document open.'); }
else {
console.log(`Document: ${doc.title}`);
console.log(`Total spreads: ${doc.spreadCount}, pages: ${doc.pageCount}`);
let i = 0;
for (const spread of doc.spreads) {
i++;
console.log(`\nSpread ${i}:`);
console.log(' Artboards:', spread.artboardCount);
console.log(' Has artboards:', spread.artboardCount > 0);
}
console.log('\nCurrent spread:', doc.currentSpread);
}