Story structure
Asking questions about the manuscript, and building the boards, timelines, flow maps, collections and scenes a writer organises a book with.
On this page
Around the prose, a writer builds scaffolding: cardboards of cards, plot grids, timelines, flow
maps, collections of notes, scenes inside chapters. Plugins can read all of it through
tintero.project, and since 0.10.5 they can also build it. On top of that there is a way to ask
questions about the manuscript without cross-referencing raw data yourself.
Three namespaces carry this, and each one does a different job:
| Namespace | What it does | What it returns |
|---|---|---|
tintero.knowledge | Answers questions about the manuscript (“which chapters is Kaela in?”) | Text, already written in English |
tintero.structure | Writes timelines, flow maps, collections, scenes and notes | The entity it created, or nothing |
tintero.boards | Writes cardboards and plot grids | The entity it created, or nothing |
Reading stays where it was: tintero.project.getTimelines(), getFlowMaps(), getCollections(),
getScenes(fileId), getNotes(), getCardboards() and getPlotGrids(). You will need them to get the
ids every write method asks for.
Asking about the manuscript
tintero.knowledge resolves things the way a person names them: “chapter 7”, a fragment of a
title, a character’s name. Every method returns a string in English, phrased to be read by a
person or a language model, not parsed. If you need the data itself, use tintero.project.
const chapters = await tintero.knowledge.listChapters();
const chapter = await tintero.knowledge.findChapter('chapter 7');
const where = await tintero.knowledge.chaptersWithCharacter('Kaela');
const profile = await tintero.knowledge.getCharacterProfile('Kaela');
const text = await tintero.knowledge.getChapterText('chapter 7');
Each method needs the read permission for what it reads: project.read.files
for the chapter list and synopses, project.read.fileContent for prose,
project.read.characters for characters,
project.read.worldbuilding for worldbuilding and
project.read.docContent for getDocument().
A few behaviours worth knowing:
- A miss is usually an answer, not an error.
getCharacterProfile('Nobody')orfindChapter('chapter 99')resolve to a sentence saying nothing matches. OnlygetChapterText()andsummarizeChapter()reject when no chapter matches. Every method rejects when no project is open, and every required argument left empty is rejected too (characterAppearanceCounts()takes an optional one: without it, it counts the whole cast). - Answers are clipped.
getChapterText()andgetDocument()cut the prose at 20,000 characters and append[...].listChapters(),findChapter(),getChapterSynopsis(),chaptersWithCharacter()andcharacterAppearanceCounts()are cut at 4,000 characters with a notice saying the answer was truncated, so on a long book they can be incomplete. To read a whole chapter, usetintero.project.getFileContent(). - Prefer the synopsis.
getChapterSynopsis()returns what the author wrote about the chapter and is far cheaper than its prose.
Summaries cost the user
summarizeChapter() is the only method here that can call a model. It needs
ai.summarize and project.read.fileContent . If the
author already wrote a summary it returns that, without calling anything. Otherwise it asks the AI
server configured in Tintero’s settings, which spends the user’s time and quota.
Searching the prose
tintero.project.searchProse() looks for a word in the text of the chapters, not in the metadata.
It matches whole words, ignores accents and accepts several forms of a name at once. Terms
shorter than three characters are dropped, and with no usable term the result is empty:
const hits = await tintero.project.searchProse(['Kaela Voss', 'Kaela'], { maxExcerptsPerChapter: 2 });
for (const hit of hits) {
console.log(hit.title, hit.count, hit.excerpts);
}
It reads at most 250 chapters by default (maxChapters, counted in manuscript order after the
chapterIds filter) and returns up to 3 excerpts per chapter (maxExcerptsPerChapter). It needs
project.read.fileContent .
Building structure
Every write goes through the same services Tintero’s own panels use, so a plugin’s change is saved, synced and shown in an open view exactly like one the writer made by hand. There is no undo a plugin can call, and a deleted board, timeline or flow map is gone.
Methods check the container you name (the timeline, lane, flow map, collection, board, cell or scene) and reject when it does not exist. Most messages name the missing id; the collection methods reject with a short error code instead. Not every other id is checked:
removeTimelineBlock(),removeFlowMapNode()andremoveFlowMapConnection()resolve without doing anything when the id does not exist.structure.deleteNote()resolves too, removes nothing, but still saves and announces a deletion for that id.- The ids you point at are stored as given, unchecked: every id inside a timeline block
(
fileId,eventId,sceneId,characterId,worldbuildingId), theparentIdofcreateTimeline(), the linked ids of a flow-map node, the characters, location and objects of a scene, and the item ofaddToCollection(). The exceptions are cardboard references and a plot grid’schaptercells, which are checked. Check them againsttintero.projectfirst, or you will save a link to nothing.
Ids for new nodes, blocks, milestones and segments are assigned by Tintero; you get them back in the returned object.
Timelines
project.write.timelines . A timeline has lanes (strands of the story), and lanes hold blocks: a chapter, a scene, or a worldbuilding event.
const timeline = await tintero.structure.createTimeline('Main plot');
const lane = await tintero.structure.addTimelineLane(timeline.id, 'Kaela');
await tintero.structure.addTimelineBlock(timeline.id, lane.id, { kind: 'document', fileId, startCol: 0, spanCols: 2 });
await tintero.structure.addTimelineMilestone(timeline.id, { text: 'The betrayal', position: 3 });
await tintero.structure.addTimelineSegment(timeline.id, { name: 'Act I', startCol: 0, spanCols: 4 });
A document block needs fileId, an event block needs eventId. A milestone always spans every
lane. setTimelineAxis() only changes the labels along the axis (unit, start, step): they
are never real dates. Pass null to remove them. deleteTimeline() also deletes every timeline
nested under it.
Flow maps
project.write.flowmaps . Two ways to build one. Node by node:
const map = await tintero.structure.createFlowMap('Who knows what');
const a = await tintero.structure.addFlowMapNode(map.id, { label: 'Kaela', x: 0, y: 0 });
const b = await tintero.structure.addFlowMapNode(map.id, { label: 'The letter', x: 240, y: 0 });
await tintero.structure.addFlowMapConnection(map.id, { fromNodeId: a.id, toNodeId: b.id, label: 'finds' });
Or all at once, with tintero.project.updateFlowMap(), which replaces every node and connection.
It checks the whole payload before writing anything, so a malformed one leaves the map untouched.
Unlike addFlowMapNode(), it fills in nothing: you supply every id (unique), every node’s x, y,
type and color, and every connection’s arrowType.
Give nodes coordinates: in addFlowMapNode(), x and y default to 0, so nodes without them
pile up on top of each other. A connection must point at nodes that already exist. removeFlowMapNode() removes the arrows
attached to the node along with it.
tintero.editor.insertFlowMap() places a read-only view of a flow map in the open document, and
needs editor.write . It rejects when no document is open in the editor or
the flow map does not exist, and leaves the document with unsaved changes.
Collections, notes and scenes
- Collections: project.write.collections .
addToCollection()takes the item’s id and whether it is a chapter ('file') or a document ('doc'); it rejects an item that is already there, andremoveFromCollection()rejects one that is not. Deleting a collection leaves its chapters and documents alone, and shows the user a notice. - Notes: project.write.notes .
project.addNote()creates them;structure.updateNote()andstructure.deleteNote()are new.updateNote()rewrites the whole body and rejects if you leave outcontent, so read the note first if you only mean to change part of it. A note anchored in a chapter is marked in its prose, sodeleteNote()also rewrites and saves that chapter to remove the mark. - Scenes: project.write.scenes .
createScene()needs the id of an existing chapter, and defaultstypeto'scene'.updateScene()changes only the fields you pass and ignores anything that is not a scene field.
Boards
tintero.boards writes the two kinds of board in the Boards panel.
Cardboards
project.write.cardboards . A grid of cards. Rows and columns are addressed by index (from 0), cards by their cell id.
const board = await tintero.boards.createCardboard('Act structure', 3, 4);
const cell = board.cells[0];
await tintero.boards.updateCardboardCell(board.id, cell.id, { title: 'Inciting incident', tags: ['act-1'] });
await tintero.boards.addCardboardReference(board.id, cell.id, 'chapter', fileId);
await tintero.boards.addCardboardNote(board.id, cell.id, { kind: 'checkbox', title: 'Foreshadow the letter' });
await tintero.boards.setCardboardHeader(board.id, 'col', 0, 'Act I', '#e0a458');
rows and cols default to 3 and must be whole numbers from 1 to 50; anything else is rejected,
not clamped. A cardboard keeps at least one row and one column. updateCardboardCell() only
applies title, status, tags (which must be an array) and color, and drops anything else.
A reference’s type is 'chapter', 'doc', 'character' or 'worldbuilding', and its target must exist.
Notes can be sticky or checkbox; image, audio and PDF notes are not available to plugins.
Plot grids
project.write.plotgrid . Chapters down one side, plot threads across. The
first column (position 0) is the chapters column: it takes chapter cells (or unassigned, the
default type), and cannot be deleted or moved. The other columns take any cell except chapter. A chapter cell needs the chapter’s
id in referenceId, and rowIndex must be an existing row: call addPlotGridRow() before writing
past the last one.
const grid = await tintero.boards.createPlotGrid('Threads');
const thread = await tintero.boards.addPlotGridColumn(grid.id, 'The letter');
await tintero.boards.setPlotGridCell(grid.id, thread.id, 0, { type: 'note', content: 'Kaela hides it' });
await tintero.boards.addPlotGridRowNote(grid.id, 0, { content: 'Part one' });
A new plot grid starts with the chapters column, two empty plot columns and three rows. A row
note is a banner above a row; a rowIndex equal to the number of rows puts it after the last one.
Opening things for the user
tintero.editor.open(id) opens a document, character, worldbuilding element, image, audio file or
PDF, as if the user had clicked it in the file tree. It needs editor.read ,
and rejects an id that nothing in the open project has. From an app view the user has already
left, it rejects whatever the id.
Called from an app view, it hides your view rather than closing it; the
Surfaces guide explains what that means for your plugin.
Every method, with its exact signature, is in the API reference.