Profiles and scripts
Scripting reference
Everything a Samurai Scribe script can use in scripting API 1.6: the meeting, transcript, notes, model, your vault, calendar, standard library, limits and versions.
On this page
- A script’s shape
- Constants
- RunContext
- Meeting
- Transcript
- Note
- Llm
- CalendarEvent
- Vault
- Members
- Paths
- A note from the vault
- What a step may change
- Modules
- Other functions
- Standard library
- chunk_to_fit
- reduce_notes
- attachments_text
- calendar_block
- dictionary_block
- build_notes_block
- clean_summary
- chunk_turns
- filter_turns
- best_pause_cut
- format_time
- join_lines
- join_notes
- is_heading
- is_placeholder_line
- contains_digit
- Errors
- Limits
- API versions
This page lists everything a script can use in Samurai Scribe 0.65.0, which runs scripting API 1.6. For what scripts are and how to make one, start with Scripting; for complete examples, see the cookbook.
Scripts are written in Rhai. Rhai’s own built-in functions for strings, numbers, arrays and maps all work. Beyond them, a script reaches the meeting and your vault only through the objects on this page: it can’t reach the network by itself, and beyond its own meeting it sees only what folder access lets AI see. Rhai’s eval is turned off. Members added after API 1.4 are marked with the version that added them.
A script’s shape
const API_VERSION = "1.6"; // the scripting API this script is written for
const TEMPERATURE = 0.3; // optional: 0.0 (steady) to 1.0 (varied)
fn run(ctx) {
let meeting = ctx.meeting;
let transcript = meeting.transcript;
let summary = meeting.create_note("Summary");
meeting.llm.write(summary, "Summarize this meeting.\n\n" + transcript.text());
}
- A script must define
fn run(ctx). Without it, Validate reports “Function run(context) is required but not defined”. runis called once per run. What it returns is ignored; what it writes to notes is what’s saved.- A script can define as many other functions as it likes. A function can’t see variables from outside it; pass them in. A top-level constant can be read inside a function as
global::NAME.
Constants
A script sets these at its top level with const. All are optional except that every script should declare API_VERSION.
API_VERSION(text): the scripting API the script is written for, such as"1.6". See API versions.TEMPERATURE(decimal number): how varied the model’s wording is, from0.0to1.0. Without it, scripts use0.4. Write it with a decimal point: a whole number such as1is ignored.CONTEXT_SIZE(whole number): how many tokens of text the model may be given at once. Without it, scripts use the context window set in Settings → AI Models for the summary model, even when the script setsMODEL, so a script withMODELshould setCONTEXT_SIZEtoo. It decidesllm.context_window, and so whatllm.fitsand the chunking functions allow.MODEL(text): a model for this script to use instead of the one in Settings → AI Models, writtenprovider/model, such as"ollama/llama3.1:8b". A name without a provider is an Ollama model. The model must be set up on this computer. LeaveMODELout unless you need it: a profile is often used on several computers. A hosted model sends what the script asks it to that service.
RunContext
ctx, what run(ctx) receives: the run.
ctx.meeting(Meeting): the meeting being written up.ctx.meetings(array of Meeting): the meetings in this run. It holds one meeting, the same one asctx.meeting.ctx.vault(Vault, since 1.6): your whole vault, beyond the meeting’s folder. Reading is always allowed; changing is up to the step’s settings. See Vault.ctx.progress(fraction, message): reports how far the script has got.fractionruns from0.0to1.0over the script’s own work, written with a decimal point (a whole number such as1is an error);messageshows with the run’s progress, such as"Writing the summary…". A script that never calls it still shows progress, estimated as it writes.
Meeting
-
transcript(Transcript, read-only): what was said. -
attachments(array of Note): the documents and files attached to the meeting under “Add optional meeting notes & attachments…”, in the order you put them in. A meeting you have never attached anything to gives itsNotesdocument, if it has one. Each arrives as text:- a document, as its text;
- a PDF, Word (
.docx), PowerPoint (.pptx) or text file, as the text read from it; - an image, as the text read out of it. Pictures inside attached files, and images pasted into an attached note, are read too. An image is read only when something can read it: the built-in model’s image reading, an Ollama or LM Studio model that can see images, or a hosted model you have allowed to read images. Otherwise its text says why it wasn’t read.
Attached files can be read but not written.
-
create_note(name)(Note): opens the note calledname, creating it the first time. The same name gives the same note, so a script can reach it from anywhere. A new note starts empty, with one exception: the name of an attached document gives that document, with its text, and writing to it changes your document. See Note for the rules on names. -
read_note(name)(text): the text of the document callednameas the meeting’s folder held it when this step started, including what earlier steps of the run wrote.""when there is none. Reading never opens or changes the document. -
title(Note): the meeting’s name. Write to it to rename the meeting. The new name is ignored whiletitle_lockedis true. -
title_locked(true or false): true when you renamed the meeting yourself, or when it is linked to a calendar event. A script shouldn’t rename it then. -
llm(Llm): the meeting’s model, ready to use. Each read gives a fresh copy, so keep one withlet llm = meeting.llm;to change its settings. -
calendar(CalendarEvent): the calendar event the meeting is linked to. Always present; checkcalendar.is_linked. -
created_at(text): when the meeting happened, in local time, such as2026-10-28 14:05:09: its calendar event’s start, else when its recording began. Up to 0.64.1 it was the time the run started, so a meeting run on a later day was dated that day. -
date(text, since 1.6): the day the meeting is filed under, such as2026-10-28. With a calendar event, it is the event’s day. Daily notes are usually named this way. -
folder(text, since 1.6): the meeting’s folder, from the top of the vault, such asMeetings/2026-10-28 Design Review, wherever it is filed. -
link(text, since 1.6): a link to the meeting’s own note, such as[[2026-10-28 Design Review]], ready to write into another note. Obsidian follows it to the meeting. -
dictionary(array of maps, since 1.5): how you spell your words, from the Dictionary, your lists of names, products and jargon in the vault’s Dictionary folder: each entry as#{ written, heard }, the term as it should be written and an array of what a transcript may have instead (often empty). The Dictionary’s entries come first (on the free version, the first ten), then this meeting’s names, yours and the invitees’, when the Dictionary’s Add each meeting’s names is on. Empty when you have no Dictionary. The transcript already uses these spellings when the run transcribed it; the list is for what the model writes. See dictionary_block.
Transcript
-
text()(text): the whole transcript, one line per turn:Speaker: what they said. -
text(options)(text): the same, shaped by a map of options:speakers(true or false, default true): start each line with the speaker’s name;timestamps(true or false, default false): start each line with its time, such as[12:30];fromandto(seconds): only the turns that start in that range.
For example,
transcript.text(#{ timestamps: true, to: 600 })is the first ten minutes, with times. -
turns(array of maps): each turn as#{ speaker, text, start, end }, withstartandendin seconds. -
speakers(array of text): each speaker’s name, in the order they first spoke. A speaker you haven’t named has a placeholder name, such as Mic Speaker or Speaker 2. -
speaker_stats(array of maps): each speaker as#{ name, speaking_percentage }, how much of the meeting they spent speaking. With people talking over each other, the percentages can add up to more than 100. -
word_count(whole number): the words spoken, counted between spaces, so Chinese and Japanese give very low counts. -
token_count(whole number): an estimate of the transcript’s length in tokens. -
duration_minutes(decimal number): minutes from the start to the end of the last thing said, not the length of the recording. -
language(text): the language most of what was said is in, by its English name, such as"Japanese".""when the transcript is too short or too mixed to tell.
text here is a function: write transcript.text(), with the brackets.
Note
A note is named text that a script reads or writes: a document of the meeting, an attached file, the meeting’s name, or, since 1.6, a note anywhere in your vault opened with ctx.vault.note.
text(text, read and write): the note’s text. Setting it replaces the whole note, and the meeting shows the new text straight away.append(text): adds to the end of the note, shown as it is added.title(text, read-only): the note’s name. For the meeting’s name it is"Title".hidden(true or false): whether the name starts with a dot.kind(text):"markdown"for a document,"pdf","word","powerpoint","text"or"image"for an attached file, and"title"for the meeting’s name.writable(true or false, since 1.6): whether the script may write to the note. For the meeting’s documents and its name, true unless it is an attached file. For a note from the vault, see A note from the vault.
What is saved. When the script finishes, each note it wrote is saved as a document in the meeting’s folder, named after the note: a note called Summary becomes Summary.md. When the step’s This meeting setting is Review, they wait for you to keep them instead (see What a step may change). Notes the script only read are left alone. Setting a note’s text to "" leaves the document there, empty: a script can’t delete a document. A name that starts with a dot, such as .Working, makes a hidden document: it has no tab in the meeting once the run ends, and Obsidian doesn’t show it.
Names. create_note refuses:
- names starting with
_, which belong to the app’s own files, andNotesin any capitals, which is your own document (read it throughattachments); - names containing
/or\, or any of< > : " | ? *; - names ending with a dot. Spaces at either end are trimmed first.
Give the name without .md: create_note("Summary.md") writes Summary.md.md. A name ending in the extension of a file the app reads, such as .txt, .csv, .pdf or .png, gives a note that can be read but not written.
A refused name is an error at that line, so a script can catch it.
Llm
The model a script writes with. meeting.llm is set up with the summary model from Settings → AI Models, or the script’s MODEL. A script uses one model.
write(note, prompt)(text): sendspromptto the model and appends the answer tonoteas it arrives, so the meeting shows it being written. Returns the whole answer. It can’t write to an attached file.query(prompt)(text): sendspromptand returns the whole answer, without showing it anywhere. Use it for answers the script works with before writing.fits(text)(true or false): whethertextfits comfortably in the model’s context window, with room left for instructions and the answer. True whenever the window isn’t known.model(text, read-only): the model’s name exactly as given:MODELas the script writes it, or the summary model from Settings → AI Models.temperature(decimal number, read and write): the script’sTEMPERATURE, or0.4. Set it with a decimal point:llm.temperature = 1is an error.context_window(whole number, read and write): the tokens the model may be given at once, fromCONTEXT_SIZEor the model’s setting.keep_alive(whole number, read and write): seconds the model stays loaded after a request.-1keeps it loaded;0lets it go straight away. It starts at your setting for the model.unload(): frees the model’s memory now, whenkeep_aliveis0. Otherwise the model stays for itskeep_alivetime.
The prompt goes to the model exactly as the script writes it. If the run is stopped, write and query return what had arrived so far, and nothing the script wrote is saved. If the model fails, they raise an error, which stops the script unless it catches it.
An answer with no text is an error too. write raises one whenever the model answers with nothing, such as ”… answered with no text.”, so a blank document is never saved. query raises one only when the model spent its whole answer thinking and wrote nothing; otherwise an empty answer comes back as "", for prompts that may rightly have nothing to say.
CalendarEvent
meeting.calendar: the calendar event the meeting is linked to.
is_linked(true or false): whether there is one. When false, everything below is empty.title,location,organizer(text).description(text): the invite’s description, up to about its first 2,000 bytes (fewer characters in Chinese or Japanese).attendees(array of text): everyone invited, by name, or by email address where there is no name. Being invited doesn’t mean they came.starts_at,ends_at(text): date and time with its offset, such as2026-10-28T10:00:00+01:00.
Vault
ctx.vault, since 1.6: your whole vault, beyond the meeting’s folder. A script can add a line to your daily note, keep a running list of action items across meetings, update a project’s page, or file flashcards in a deck.
const API_VERSION = "1.6";
fn run(ctx) {
let meeting = ctx.meeting;
// Today's daily note gets a link to the meeting.
let daily = ctx.vault.note("Daily/" + meeting.date);
if daily.writable {
daily.append("- " + meeting.link + "\n");
}
}
Reading is always allowed, under the folder access rules you set for AI: a Hidden folder reads as if it weren’t there. A PDF, Word file or slide deck reads as its text.
Changing is your choice, not the script’s, made on the step’s row in the profile manager. The person running a script isn’t always the one who wrote it, so a script can only ask for more review, never less.
Members
note(path)(Note): a note anywhere in the vault, opened to read and, when the step allows, to change. The same path gives the same note, and nothing is written until the script writes. A note that isn’t there yet opens empty, withexistsfalse, and writing to it creates it. Keep the note in a variable before writing to it:let n = ctx.vault.note(p); n.text = "…";.note(path, #{ review: true })(Note): the same, and every change through it waits for you, whatever the step allows. Use it for a note whose path the model chose.list(folder)(array of maps): what is in a folder, as#{ path, name, kind }.kindis"folder","markdown", or a file’s kind, such as"pdf".""is the top of the vault. A missing or Hidden folder lists nothing.list(folder, #{ recursive: true })(array of maps): everything below the folder too, up to 5,000 entries.search(query)(array of maps): your notes and meetings that match, best first, as#{ path, title, excerpt, kind, meeting_id }, at most 20.meeting_idis empty for a note outside a meeting.search(query, #{ limit: 10, folder: "Projects" })(array of maps): the same, at mostlimitresults (up to 100), and only withinfolder.folders(map): the app’s folders, wherever you moved them:meetings,notebooks,unfiled,people,skills,dictionary,scriptsandprofiles. Empty for one that isn’t set up yet. Use these rather than spelling the folders’ names.write_mode(text): what the step’s Elsewhere setting allows:"ask","review"or"accept_edits".
An option a call doesn’t take, such as a misspelt one, is an error.
Paths
A path is relative to the top of the vault, with / between folders: "Daily/2026-10-28". .md is added when the name has no extension, or one the app doesn’t read. "../People/Ana" is in the folder your vault sits in, which a script reaches only while Include Parent Folder is on in Settings → AI Apps → Folder Access. A path that starts at the root of the computer, or uses .. anywhere else, is refused.
A note from the vault
A note from ctx.vault.note works like the meeting’s documents: text, text =, append and llm.write all work on it. Its text is the note’s body. Its frontmatter is its properties, changed one key at a time, so rewriting a note’s body never drops its tags. It also has:
path(text): where it is, from the top of the vault, with its extension:"Daily/2026-10-28.md".exists(true or false): whether it was there when opened, or has been written by this run. False once this run moved or deleted it.writable(true or false): whether this run may change it: the step allows changes, AI may edit its folder, it is a Markdown note outside this meeting’s folder, and the run hasn’t reached its limit.properties(map): the frontmatter, as text, numbers, true or false, and arrays of text. An empty value is(). It is a copy: change it with the functions below.set_property(key, value): sets one key to text, a number, true or false, or an array of text, leaving the rest of the frontmatter as written. Keys are letters, digits,_and-, starting with a letter. Keys starting withsamurai_, and the keys the app writes into its own files, can’t be changed.remove_property(key): takes one key out. Nothing happens when the note doesn’t have it.move_to(path): moves the note. A move always waits for you.delete(): deletes the note, to the trash. A delete always waits for you.
A note can be changed, moved or deleted in one run, not two of these. On the meeting’s own documents and its name, these members raise an error, except writable.
What a step may change
Each Script and skill step in a profile has two settings, This meeting and Elsewhere, on its row in the Summarization panel of the profile manager.
Elsewhere decides what happens to the script’s changes to other notes in the vault:
- Ask: the step reads other notes and changes none. A change is refused at the line that makes it.
- Review, the default: changes wait for you, and so do the steps after this one.
- Accept edits: edits and new notes are made, and can be undone. Moves and deletes still wait for you. Choosing it asks first.
This meeting decides what happens to the documents the script writes into its meeting with create_note:
- Auto, the default, and Accept edits: they are saved as the step ends.
- Review: nothing in the meeting is written until you keep it, the Summary included, and the steps after this one wait for you.
While a run waits for you, its status reads Waiting for your review. See Changes, modes and undo for keeping and undoing changes.
Whatever the settings:
- Only folders AI may edit can be changed, as set in folder access, and only Markdown notes. A file whose name starts with
_can’t be changed. - Changes to the Scripts, Skills and Dictionary folders always wait for you.
- The meeting’s own folder isn’t changed through
ctx.vault. Write its documents withmeeting.create_note(name). - A new note can’t go loose at the top of the Meetings or Skills folder. Put it in a meeting’s folder, a skill’s folder, or anywhere else.
Changes are made when the step ends, not as the script runs. Each one is checked at the line that makes it, so a refused change is an error the script can catch with try. Until the step ends, the script reads back what it changed: text, exists and list all see it. A run that fails or is stopped changes nothing outside its meeting. Running the step again on the same meeting replaces what its last run left waiting.
In the script editor, Test reads your vault for real and changes nothing: what the script would change outside the meeting is listed in the console’s Vault tab, checked as a step set to Review would check it. Validate has no vault: note opens empty notes and list and search find nothing.
Modules
Since 1.6, functions you use in several scripts can go in a module: a .rhai file in your Scripts folder, brought in with import.
// Scripts/lib/cards.rhai
fn card(question, answer) {
"#flashcards\n" + question + "\n?\n" + answer
}
// Scripts/Flashcards.rhai
const API_VERSION = "1.6";
import "lib/cards" as cards;
fn run(ctx) {
let note = ctx.meeting.create_note("Flashcards");
note.text = cards::card("What is the retry budget?", "Three attempts.");
}
- The path is relative to the file that imports it: the Scripts folder for a script, the module’s own folder for a module. Use
/between folders and leave out.rhai...moves up a folder. - Modules come from the Scripts folder and nowhere else: not from above it, not from a folder whose name starts with a dot, and not from elsewhere on the computer. Changes AI makes to the Scripts folder always wait for you, so a module can’t be changed behind your back.
- Keep modules in a subfolder, such as
Scripts/lib/. The script picker lists the files at the top of the Scripts folder, so a module there would show up as a script. - A module isn’t a script and has no
run, so check it with Validate from a script that imports it. - A module can use everything a script can, including the standard library. Call its functions as
alias::name(…). A module that imports itself, through other modules, is refused.
Other functions
estimate_tokens(text)(whole number): an estimate of the tokens intext, worked out the same way astoken_countandfits.print(value)anddebug(value): show a value in the editor’s Problems tab while you test.debugadds where in the script it was called.
Standard library
These functions are part of every script, with nothing to import. Call them as functions, with the data first: chunk_to_fit(transcript, llm), not transcript.chunk_to_fit(llm). To change how one works, define a function with the same name in your script: yours is used instead.
chunk_to_fit
chunk_to_fit(transcript, llm) → array of text. Splits the transcript into chunks that each fit llm, as llm.fits judges. Each cut is made at the longest pause near the end of a chunk, rather than mid-thought, and the last few turns before a cut are repeated at the start of the next chunk. A short meeting gives one chunk. Each chunk is Speaker: text lines. A single turn too long for the model becomes a chunk of its own, still too long, and a short chunk carries nothing over into the next.
reduce_notes
reduce_notes(llm, parts, condense_prompt) → text. Joins an array of notes on the parts of a meeting into one text that fits llm. When they don’t fit together, it asks the model to condense them in batches, with condense_prompt as the instruction, round after round until they fit or several rounds have passed, so the result can still be too long. A single part comes back unchanged. It uses llm.query, so nothing is shown while it works. Use it to prepare notes, then llm.write the final document from them.
attachments_text
attachments_text(meeting) → text. Everything attached to the meeting, in your order, as one text. With several attachments, each is under a ## name heading; one attachment has no heading. Empty attachments are left out.
calendar_block
calendar_block(meeting) → text. What the calendar invite says, ready to add to a prompt: the event’s title, who was invited, and its description. It is labelled so the model doesn’t read the invitations as a list of who attended. "" when no event is linked.
dictionary_block
dictionary_block(meeting) → text. Since 1.5. Your Dictionary as lines for a prompt, each term with what the transcript may have written instead, under a line asking the model to spell them as written. It stops at about 1,500 characters, ending with how many more there are. "" when meeting.dictionary is empty. The built-in script adds it to what it tells the model.
build_notes_block
build_notes_block(notes) → text. Wraps notes in a block that asks the model to treat them as your priorities, and to trust them over the transcript on names and facts. "" when notes is empty.
clean_summary
clean_summary(text) → text. Removes every ## section whose body is empty or only placeholders, as is_placeholder_line judges each line, and trims the text. That includes a section of only very short lines with no digits, such as - Budget approved. Text without ## headings comes back unchanged. Table rows count as content.
chunk_turns
chunk_turns(turns, max_chars) → array of text. Splits transcript.turns into Speaker: text chunks of at most max_chars characters, always between turns. A simpler chunk_to_fit, sized by characters instead of by the model.
filter_turns
filter_turns(turns, speaker_name) → array of maps. The turns spoken by one speaker: filter_turns(transcript.turns, "Ana Lopez").
best_pause_cut
best_pause_cut(turns, start, end) → whole number. Where chunk_to_fit cuts: the index of the turn with the longest silence before it, searched in the later part of the range from start to end. When the turns there run straight on, it is still a turn in that part of the range.
format_time
format_time(seconds) → text. Seconds as M:SS, or H:MM:SS from an hour: format_time(75) is 1:15.
join_lines
join_lines(parts, separator) → text. An array of text joined with separator between each.
join_notes
join_notes(parts) → text. An array of text joined with a blank line between each.
is_heading
is_heading(line) → true or false. Whether a line is a ## heading.
is_placeholder_line
is_placeholder_line(line) → true or false. Whether a line carries no content: blank, a placeholder such as “None” or “N/A”, or a very short line with no digits.
contains_digit
contains_digit(text) → true or false. Whether text contains a digit from 0 to 9.
Errors
- Catching.
try { … } catch (err) { … }catches an error so the script can carry on. An error raised by Samurai Scribe, such as a refused note name or a failed model request, arrives as text. Rhai’s own errors arrive as a map with amessage. - An error the script doesn’t catch ends it. Nothing it wrote is saved, and the run stops with an error, such as “Summary generation failed: Script run() error: …”. In a profile with several steps, the documents from the steps before it are kept.
- A refused change to the vault, such as a change the step doesn’t allow or one in a Read only folder, is an error at that line, with the reason, so a script can catch it.
- A script that doesn’t compile isn’t run. The meeting is written up with the built-in script instead, and the run says so, with the compiler’s error and where it is.
- Validate catches compile errors, and errors on the way through a small sample meeting. It can’t catch an error that only real meetings reach, such as a long meeting’s chunking path.
Limits
- Notes: a run can open at most 24 notes of its own. The attached documents it reads don’t count toward them.
- Vault: a run can change at most 200 notes outside its meeting.
listgives at most 5,000 entries, andsearchat most 100 results. - Attachments: the text of an attached file is read up to its first 200,000 characters. A run reads 12 images among its attachments, attached image files first, and says how many it skipped.
- Calendar: about the first 2,000 bytes of an invite’s description.
- Work: a script stops with an error after 10,000,000 operations, a likely endless loop.
- Size: a text value can hold up to 10,000,000 bytes (fewer characters in Chinese or Japanese), and an array or map up to 1,000,000 items.
- Nesting: expressions nest up to 128 levels deep, and 64 inside functions.
API versions
A script says which version of the scripting API it is written for with API_VERSION. Samurai Scribe 0.65.0 runs API 1.6.
- A version is
MAJOR.MINOR. A third part, as in"1.6.0", is ignored. - A new minor version adds to the API, so a script written for an older one still runs. Samurai Scribe 0.65.0 runs scripts that declare 1.2, 1.3, 1.4, 1.5 or 1.6.
- 1.0 and 1.1 are no longer run: they used parts of the API that were taken away in 1.2.
- A script that declares no version is run as it is.
- A script that declares a version this app doesn’t run, such as 1.1, or 1.7 from a newer app, isn’t run. The meeting is written up with the built-in script instead, and the app says why: “Your script declares scripting API 1.7; this app runs 1.6. The built-in default script was used instead — update the script’s API_VERSION…”
To fix that, update Samurai Scribe if the script came from a newer version, or check the script against this page and set its API_VERSION to "1.6".
What each version added:
- 1.6 (Samurai Scribe 0.65.0):
ctx.vault, for reading and changing notes anywhere in the vault; on a note,path,exists,writable,properties,set_property,remove_property,move_toanddelete; on the meeting,folder,linkanddate. Modules:importreads.rhaifiles from your Scripts folder, and from nowhere else. - 1.5 (Samurai Scribe 0.65.0):
meeting.dictionary, and the standard library’sdictionary_block. - 1.4:
transcript.language. - 1.3:
note.kind, and attached files (PDF, Word, PowerPoint, text and images) inmeeting.attachments, as notes that can be read but not written.
Something wrong, out of date or missing on this page? Email support@samuraiscribe.com or ask in the Discord community.