Commit one complete canvas change.
Group every node, relation, and record mutation for one user intent into a single undoable transaction.
Trigger the transaction from a browser DOM editor.
Name the user intent
Commands, editors, importers, and agent actions read immutable snapshots. They change the document only through action.document.transaction(label).
The label names the result a person can undo. Connect cards, Import schedule, and Arrange selection describe one complete change. Labels such as Update or Save hide what happened.
const transaction = action.document.transaction('Archive card');
transaction.update({
id: cardId,
fields: { status: 'archived' },
});
transaction.commit();Nothing changes before commit(). A transaction can commit once; keep it local to the operation that created it.
Stage related mutations together
One transaction can create, update, and delete nodes; bind and unbind relationships; and write node or collection records.
const transaction = action.document.transaction('Create connected cards');
const first = transaction.create({
kind: 'example:card',
x: 120,
y: 180,
width: 220,
height: 120,
fields: { title: 'Draft' },
});
const second = transaction.create({
kind: 'example:card',
x: 460,
y: 180,
width: 220,
height: 120,
fields: { title: 'Review' },
});
const connector = transaction.create({
kind: 'example:connector',
x: 290,
y: 180,
width: 120,
height: 1,
});
transaction
.bind({
child: connector,
role: 'source',
type: 'example:connect',
parent: first,
payload: { side: 'east' },
})
.bind({
child: connector,
role: 'target',
type: 'example:connect',
parent: second,
payload: { side: 'west' },
})
.commit();create(...) returns a transaction-local id. That id can be used by later operations in the same transaction, which lets the bindings commit with their new nodes instead of requiring another round trip.
Choose Create connected cards. The preview commits both cards, the connector, and both endpoint bindings as one document change.
import { definePlugin, hexColor, type PluginActionContext } from '@editful/canvas-sdk';
import {
BINDING_CARD_KIND,
CONNECTOR_BINDING,
CONNECTOR_KIND,
registerFlowKinds,
} from './node-bindings';
export default definePlugin({
register(context) {
registerFlowKinds(context);
context.editor({
id: 'example:transaction-builder', label: 'Transaction builder',
surface: 'left-sidebar',
mount(container, action) {
const summary = document.createElement('p');
const create = document.createElement('button');
summary.textContent = 'Two nodes + two bindings + one undo entry';
create.textContent = 'Create connected cards';
create.addEventListener('click', () => createConnectedCards(action));
container.replaceChildren(summary, create);
return () => container.replaceChildren();
},
});
},
});
export function createConnectedCards(action: PluginActionContext): void {
const transaction = action.document.transaction('Create connected cards');
const first = transaction.create({
kind: BINDING_CARD_KIND, x: -100, y: 0, width: 150, height: 86,
fill: hexColor('#f2c45d'), stroke: hexColor('#171815'),
fields: { title: 'Draft' },
});
const second = transaction.create({
kind: BINDING_CARD_KIND, x: 130, y: 0, width: 150, height: 86,
fill: hexColor('#ff7468'), stroke: hexColor('#171815'),
fields: { title: 'Review' },
});
const connector = transaction.create({
kind: CONNECTOR_KIND, x: 15, y: 0, width: 80, height: 1,
});
transaction
.bind({ child: connector, role: 'source', type: CONNECTOR_BINDING,
parent: first, payload: { side: 'east' } })
.bind({ child: connector, role: 'target', type: CONNECTOR_BINDING,
parent: second, payload: { side: 'west' } })
.commit();
}Let revision checks protect the commit
The transaction captures the current semantic document revision when it starts. At commit time, Editful verifies that revision and every referenced node. If another completed change made the snapshots stale, the commit throws PluginActionConflictError and writes nothing.
Read the required snapshots before opening the transaction, validate them, then stage and commit without waiting on network or user input.
const [card] = action.document.inspect([cardId]);
if (card?.kind !== 'example:card') {
throw new Error('Card is unavailable');
}
const transaction = action.document.transaction('Rename card');
transaction.update({ id: card.id, fields: { title: nextTitle } });
transaction.commit();Collect asynchronous input first. A transaction is a short synchronous commit boundary, not a container to keep open across prompts or requests.
Use the smallest mutation set
create(...)- Stages one plugin-owned node and returns a local id.
update(...)- Stages geometry, style, or declared field changes on a live node.
delete(...)- Stages removal of one live node.
bind(...)- Sets one role-keyed parent relation and its JSON payload.
unbind(...)- Clears one child role.
setRecord(...)- Writes or deletes JSON owned by one node.
setCollectionRecord(...)- Writes or deletes JSON in a shared document collection.
Keep one transaction aligned with one operation. Separate unrelated changes so undo history, agent feedback, and collaboration events retain meaningful boundaries.