CSV importer
Turn a user-selected file into Notes through the host-owned select, map, preview, confirm and retry workflow.
An importer parses a file into Note previews. GhostBoard owns everything around it: the native file picker, field mapping, the immutable preview, confirmation, destination, retries, rollback and cleanup of the file handle. Your code never writes workspace content directly.
Scaffold with bunx create-ghostboard-plugin my-importer --importer.
Register
app.importers.register({
id: "notes-csv",
title: "Import Notes from CSV",
accept: [{ description: "CSV files", extensions: [".csv"] }],
async inspectFields(file, { signal }) {
const text = await app.files.readText(file, { signal });
const fields = parseHeader(text);
const find = (name) => fields.find((f) => f.toLowerCase() === name) ?? null;
return {
fields,
suggestedMapping: {
title: find("title"),
text: find("text"),
color: find("color"),
},
};
},
async parse(file, { mapping, signal }) {
const text = await app.files.readText(file, { signal });
return parseCsvToNotes(text, mapping); // { notes, warnings } or { segments, warnings }
},
});
The user’s flow
- The user picks Import Notes from CSV in the Plugin Workbench. If several importers accept the same extension, the host asks which one to use.
- The host opens the native file picker and hands your importer an opaque, read-only file handle bound to this plugin, workspace, activation and process. You get
name,size,mimeTypeandlastModified, never a path. - If you implemented
inspectFields, the host shows a mapping UI for the fixed Note surface: requiredtitle, optionaltextandcolor. One source field cannot feed two targets; at most 100 unique fields are offered. parsereturns Notes and warnings. The host shows an immutable preview with counts, titles, body excerpts, colors and warnings. Nothing has been written yet; cancelling here writes nothing.- On confirm, the host creates the Notes as detached Notes (in the library, not on a canvas) in one atomic 1–100 item transaction with a stable operation ID. For a single batch the user may also choose to place the new Notes on the open canvas in a grid or column; creation and placement are separate operations, so a placement failure leaves the Notes created and retryable.
- The result is recorded with a batch ID. The user can undo it from the plugin batch history; disabling or uninstalling the importer keeps the Notes.
More than 100 rows
Return explicit segments instead of one list:
return {
segments: chunk(notes, 100).map((notes) => ({ notes })),
warnings,
};
Allowed: 2–50 segments of 1–100 Notes each (101–5,000 Notes total). The host commits segments in order, shows the completed range, deduplicates retries by segment operation ID and resumes from the failed segment after a lost response. Rollback runs completed ranges in reverse; a range that the user has since edited or referenced is kept whole while the others are removed. Segmented imports always stay detached.
Limits and rules
readTextis strict UTF-8 and defaults to 1 MiB (maxBytesup to 16 MiB); usereadBytesorstreamfor binary or large files.- The
textyou return is plain text split into paragraphs, not Markdown. - Do not write workspace content, keep handles, or start background work in
inspectFields/parse. Cancel promptly whensignalaborts. - Handles are released when the session ends, the plugin reloads or is disabled, or the workspace switches.
Testing
const fake = createFakeHost({ pluginId: "yourname.csv", workspaceId: "w1" });
await plugin.setup(fake.app);
const file = fake.harness.files.addText("notes.csv", "title,text\nOne,Body", {
mimeType: "text/csv",
});
const fields = await fake.harness.behavior.inspectImporterFields(
"notes-csv",
file,
);
const preview = await fake.harness.behavior.parseImporter("notes-csv", file, {
mapping: fields.suggestedMapping,
});