Tintero Developers

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:

NamespaceWhat it doesWhat it returns
tintero.knowledgeAnswers questions about the manuscript (“which chapters is Kaela in?”)Text, already written in English
tintero.structureWrites timelines, flow maps, collections, scenes and notesThe entity it created, or nothing
tintero.boardsWrites cardboards and plot gridsThe 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') or findChapter('chapter 99') resolve to a sentence saying nothing matches. Only getChapterText() and summarizeChapter() 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() and getDocument() cut the prose at 20,000 characters and append [...]. listChapters(), findChapter(), getChapterSynopsis(), chaptersWithCharacter() and characterAppearanceCounts() 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, use tintero.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() and removeFlowMapConnection() 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), the parentId of createTimeline(), the linked ids of a flow-map node, the characters, location and objects of a scene, and the item of addToCollection(). The exceptions are cardboard references and a plot grid’s chapter cells, which are checked. Check them against tintero.project first, 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, and removeFromCollection() 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() and structure.deleteNote() are new. updateNote() rewrites the whole body and rejects if you leave out content, 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, so deleteNote() also rewrites and saves that chapter to remove the mark.
  • Scenes: project.write.scenes . createScene() needs the id of an existing chapter, and defaults type to '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.