Profiles and scripts

Scripting cookbook

Complete Samurai Scribe scripts to copy and change, each checked with Validate in 0.65.0: languages, long meetings, notes in your vault, stats and more.

Checked with version 0.65.0
Use with AI
On this page
  1. A summary in the meeting’s language
  2. Several documents from one run
  3. Long meetings
  4. Using your notes, attachments and calendar
  5. Chapters with times
  6. Meeting stats, without the model
  7. Building on an earlier step
  8. A line in your daily note
  9. A running list of action items
  10. Naming the meeting
  11. When the model fails
  12. Who spoke

Each recipe on this page is a complete script for scripting API 1.6, and each passes Validate in Samurai Scribe 0.65.0. To use one:

  1. Make a new script: in the profile manager’s Script step, click the script button, type a name and press New. See Choosing a script for a profile.
  2. Click the Script step to open the script, replace everything with the recipe, and change it to suit you.
  3. Press Validate, then Test on a meeting with a transcript.
  4. Close the editor, press the Save script icon at the right end of the script button, and save the profile.

What each function and property does is in the scripting reference.

A summary in the meeting’s language

For meetings held in more than one language. transcript.language names the language most of the meeting was in, so the script can ask for that language by name. It is empty when the transcript is too short or too mixed to tell, and the script then asks for the transcript’s own language.

// A summary written in the language the meeting was held in.
const API_VERSION = "1.6";
const TEMPERATURE = 0.3;

fn language_rule(language) {
    if language == "" {
        return "Write in the same language as the transcript.";
    }
    "The meeting is in " + language + ". Write everything in " + language + "."
}

fn run(ctx) {
    let meeting = ctx.meeting;
    let transcript = meeting.transcript;
    if transcript.word_count == 0 { return; }

    let rule = language_rule(transcript.language);
    let summary = meeting.create_note("Summary");
    // Say the language at the start and again at the end: on a long
    // transcript, the last instruction is the one a model keeps to.
    meeting.llm.write(summary, rule
        + "\nWrite short meeting notes in Markdown: an overview, then the main points as bullets.\n\n"
        + "Transcript:\n" + transcript.text()
        + "\n\n" + rule);
}

Several documents from one run

For when one meeting should give several documents. Each create_note makes a document, and a later request can start from what an earlier one wrote: here the email is drafted from the summary rather than from the whole transcript.

// Three documents from one run: a summary, the decisions, and a draft
// follow-up email that starts from the summary.
const API_VERSION = "1.6";
const TEMPERATURE = 0.3;

fn run(ctx) {
    let meeting = ctx.meeting;
    let transcript = meeting.transcript;
    if transcript.word_count == 0 { return; }

    let llm = meeting.llm;
    let said = "\n\nTranscript:\n" + transcript.text();

    ctx.progress(0.05, "Writing the summary…");
    let summary = meeting.create_note("Summary");
    llm.write(summary, "Summarize this meeting in five bullet points." + said);

    ctx.progress(0.4, "Listing decisions…");
    let decisions = meeting.create_note("Decisions");
    llm.write(decisions, "List each decision made in this meeting as a bullet. "
        + "If none were made, write: No decisions." + said);

    ctx.progress(0.7, "Drafting the follow-up email…");
    let email = meeting.create_note("Follow-up Email");
    llm.write(email, "Draft a short, friendly follow-up email to the attendees, "
        + "based on these meeting notes. Plain text, no subject line.\n\nNotes:\n"
        + summary.text);
}

A run can open up to 24 notes. ctx.progress keeps the run’s progress moving between documents.

Long meetings

For meetings too long for the model to read in one go. chunk_to_fit splits the transcript into parts that each fit the model, cutting at pauses. The script takes notes on each part, reduce_notes folds those notes down until they fit, or as close as several rounds get, and the summary is written from them. A short meeting skips straight to the summary.

// A summary that works however long the meeting is: notes on each part
// first, then one summary from those notes.
const API_VERSION = "1.6";
const TEMPERATURE = 0.3;

fn part_instruction() {
    "Take concise bullet-point notes on this part of a longer meeting. "
        + "Keep names, numbers, dates, decisions and who owns each task."
}

fn run(ctx) {
    let meeting = ctx.meeting;
    let transcript = meeting.transcript;
    if transcript.word_count == 0 { return; }

    let llm = meeting.llm;
    let summary = meeting.create_note("Summary");
    let ask = "Write meeting notes in Markdown: an overview, the main points, "
        + "decisions, and action items with owners.";

    // One chunk when the meeting fits the model, several when it doesn't.
    let chunks = chunk_to_fit(transcript, llm);
    if chunks.len() <= 1 {
        llm.write(summary, ask + "\n\nTranscript:\n" + transcript.text());
        return;
    }

    // A hidden note (its name starts with a dot) keeps the working notes
    // out of the way. It is still saved in the meeting's folder.
    let working = meeting.create_note(".Parts");
    let parts = [];
    for i in 0..chunks.len() {
        ctx.progress(0.05 + 0.6 * i / chunks.len(),
            "Reading part " + (i + 1) + " of " + chunks.len() + "…");
        working.append("## Part " + (i + 1) + "\n");
        parts.push(llm.write(working, part_instruction() + "\n\nTranscript:\n" + chunks[i]));
        working.append("\n\n");
    }

    // Fold the part notes down until they fit, then write the summary.
    ctx.progress(0.7, "Combining the parts…");
    let notes = reduce_notes(llm, parts, part_instruction());
    ctx.progress(0.8, "Writing the summary…");
    llm.write(summary, ask + " The notes below cover consecutive parts of one meeting.\n\nNotes:\n" + notes);
}

The part notes go in a hidden document, .Parts, so you can see what the model took from each part without it getting in the way. To make every part smaller, add const CONTEXT_SIZE = 8000; at the top: the parts are sized to it, when the model’s own window is larger.

Using your notes, attachments and calendar

For a briefing that checks the meeting against an agenda. attachments_text gives everything attached to the meeting, in your order: your notes, an agenda, a PDF or a slide deck. calendar_block gives what the calendar invite says, including who was invited.

// A briefing that uses what you attached to the meeting and what its
// calendar invite says, as well as the transcript.
const API_VERSION = "1.6";
const TEMPERATURE = 0.3;

fn run(ctx) {
    let meeting = ctx.meeting;
    let transcript = meeting.transcript;
    if transcript.word_count == 0 { return; }

    // Everything attached, in your order, each under its own name.
    let attached = attachments_text(meeting);

    // List what was attached, by kind, for the record.
    let sources = meeting.create_note("Sources");
    for doc in meeting.attachments {
        sources.append("- " + doc.title + " (" + doc.kind + ")\n");
    }

    // The invite: title, attendees, and the start of its description.
    let invite = calendar_block(meeting);

    let prompt = "Write a one-page briefing on this meeting. Check the transcript "
        + "against the agenda and notes below, and say which agenda items were not covered.";
    if attached != "" {
        prompt += "\n\nAgenda and notes:\n" + attached;
    }
    if invite != "" {
        prompt += "\n" + invite;
    }
    let briefing = meeting.create_note("Briefing");
    meeting.llm.write(briefing, prompt + "\n\nTranscript:\n" + transcript.text());
}

doc.kind tells a document (markdown) from an attached file (pdf, word, powerpoint, text or image). Attached files can be read but not written.

Chapters with times

For long recordings you go back to. text(#{ timestamps: true }) starts each line of the transcript with its time, so the model can say where each topic begins.

// Chapters: the topics of the meeting, each with the time it starts.
const API_VERSION = "1.6";
const TEMPERATURE = 0.2;

fn run(ctx) {
    let meeting = ctx.meeting;
    let transcript = meeting.transcript;
    if transcript.word_count == 0 { return; }

    // Each line starts with its time, such as [12:30].
    let timed = transcript.text(#{ timestamps: true });
    let chapters = meeting.create_note("Chapters");
    meeting.llm.write(chapters,
        "Split this meeting into chapters by topic. For each chapter write one line: "
        + "the start time in square brackets as it appears in the transcript, then a short title. "
        + "No other text.\n\nTranscript:\n" + timed);
}

Meeting stats, without the model

For numbers you want exact. Nothing here asks the model: the script works out the meeting’s length, its words, how long each person spoke as a share of the meeting (the shares needn’t add up to 100%) and their longest turn, so it runs in a moment.

// Meeting statistics, worked out without the model: length, words, and
// how much each person spoke.
const API_VERSION = "1.6";

fn run(ctx) {
    let meeting = ctx.meeting;
    let transcript = meeting.transcript;
    let stats = meeting.create_note("Stats");

    stats.append("# Meeting stats\n\n");
    stats.append("- Length: " + format_time(transcript.duration_minutes * 60.0) + "\n");
    stats.append("- Words: " + transcript.word_count + "\n");
    stats.append("- Speakers: " + transcript.speakers.len() + "\n\n");

    stats.append("## Speaking time\n\n");
    for s in transcript.speaker_stats {
        let pct = s.speaking_percentage.round();
        stats.append("- " + s.name + ": " + pct.to_int() + "%\n");
    }

    // Each person's longest turn, with when it started.
    stats.append("\n## Longest turn\n\n");
    for name in transcript.speakers {
        let longest = #{ text: "", start: 0.0 };
        for turn in filter_turns(transcript.turns, name) {
            if turn.text.len() > longest.text.len() {
                longest = turn;
            }
        }
        stats.append("- " + name + " at " + format_time(longest.start) + ": " + longest.text + "\n");
    }
}

filter_turns picks one speaker’s turns, and format_time turns seconds into M:SS.

Building on an earlier step

For a profile whose script runs after a skill. read_note reads what is in the meeting’s folder when the script’s step starts, including what the steps before it wrote. This script writes an English copy of a summary a skill wrote. Put the Script step after the skill in the profile.

// Runs after a step that wrote "Summary" (a skill, say), and writes an
// English version of it. Put it after that step in the profile.
const API_VERSION = "1.6";
const TEMPERATURE = 0.2;

fn run(ctx) {
    let meeting = ctx.meeting;
    // What the meeting's folder held when this step started, including
    // what the steps before it wrote. "" if there is no such document.
    let summary = meeting.read_note("Summary");
    if summary == "" {
        print("No Summary to translate.");
        return;
    }
    if meeting.transcript.language == "English" {
        return;
    }
    let english = meeting.create_note("Summary (English)");
    meeting.llm.write(english, "Translate these meeting notes into English. "
        + "Keep the Markdown formatting.\n\n" + summary);
}

print shows its message in the editor’s Problems tab when you test.

A line in your daily note

For a daily note that lists the day’s meetings. ctx.vault.note opens a note anywhere in your vault, here the daily note named after the meeting’s day, and creates it when there isn’t one. meeting.link is a link to the meeting that Obsidian follows. Change Daily/ to the folder your daily notes are in.

// Adds a link to the meeting to that day's daily note.
const API_VERSION = "1.6";

fn run(ctx) {
    let meeting = ctx.meeting;
    if meeting.link == "" { return; }

    let daily = ctx.vault.note("Daily/" + meeting.date);
    if !daily.writable {
        print(daily.path + " can't be changed: check the step's Elsewhere setting and folder access.");
        return;
    }
    // Run again, the meeting isn't added twice.
    if daily.text.contains(meeting.link) { return; }

    let line = "- " + meeting.link + "\n";
    if daily.text != "" && !daily.text.ends_with("\n") {
        line = "\n" + line;
    }
    daily.append(line);
}

With the step’s Elsewhere on Review, the line waits for you to keep it, and so do the steps after this one; on Accept edits it is added when the step ends. On Ask, daily.writable is false and the script says so in the editor’s Problems tab when you test. Test lists the change in the console’s Vault tab without making it.

A running list of action items

For one list of open tasks across all your meetings. The script writes the meeting’s own Action Items document, then adds the same tasks to Tasks/Open tasks.md under a link to the meeting, and records in the list’s properties when it was last added to.

// The meeting's action items, in the meeting and in one running list.
const API_VERSION = "1.6";
const TEMPERATURE = 0.2;

fn instructions() {
    "List every task someone agreed to do in this meeting.\n"
        + "Write one Markdown checkbox per task: - [ ] **Owner** — the task.\n"
        + "Use only what the transcript says. If nobody agreed to do anything, reply with nothing at all."
}

fn run(ctx) {
    let meeting = ctx.meeting;
    let transcript = meeting.transcript;
    if transcript.word_count == 0 { return; }

    ctx.progress(0.1, "Finding action items…");
    let found = meeting.llm.query(instructions() + "\n\nTranscript:\n" + transcript.text());
    found.trim();

    let items = meeting.create_note("Action Items");
    if found == "" {
        items.text = "No action items.";
        return;
    }
    items.text = found;

    // The running list, outside the meeting.
    let list = ctx.vault.note("Tasks/Open tasks");
    if !list.writable || meeting.link == "" || list.text.contains(meeting.link) {
        return;
    }
    list.append("\n## " + meeting.link + "\n\n" + found + "\n");
    list.set_property("updated", meeting.date);
}

llm.query is used rather than llm.write because the answer may rightly be empty: write treats an empty answer as an error. set_property changes one key of the list’s frontmatter and leaves the rest, tags included, as they were.

Naming the meeting

For a name taken from how the meeting opened. Writing to meeting.title renames the meeting. Check meeting.title_locked first: it is true when you named the meeting yourself, or when its name comes from your calendar, and a new name is ignored then.

// Names the meeting from its transcript, unless you named it yourself or
// it came from your calendar.
const API_VERSION = "1.6";
const TEMPERATURE = 0.2;

fn run(ctx) {
    let meeting = ctx.meeting;
    if meeting.title_locked || meeting.transcript.word_count == 0 {
        return;
    }
    let opening = meeting.transcript.text(#{ to: 600 });
    let title = meeting.llm.query("Give this meeting a title of three to six words. "
        + "Reply with the title only, no quotes.\n\nTranscript (first ten minutes):\n" + opening);
    title.trim();
    if title != "" {
        meeting.title.text = title;
    }
}

llm.query returns the answer without showing it, so the script can tidy it before using it. title.trim(); changes title in place.

When the model fails

For a script that should still leave something behind. A failed model request raises an error that ends the script, and nothing it wrote is saved. Catch the error to write something else instead.

const API_VERSION = "1.6";
fn run(ctx) {
    let meeting = ctx.meeting;
    let summary = meeting.create_note("Summary");
    try {
        meeting.llm.write(summary, "Summarize this meeting.\n\n" + meeting.transcript.text());
    } catch (err) {
        print("The model failed: " + err);
        summary.text = "The summary could not be written this time.";
    }
}

Who spoke

The smallest useful script: a list of the speakers. It shows how print helps while you write a script: press Test, and the values appear in Problems.

// The smallest useful script: a note listing who spoke. Uses print to show
// values in the editor while you test.
const API_VERSION = "1.6";

fn run(ctx) {
    let meeting = ctx.meeting;
    print("Speakers: " + meeting.transcript.speakers);
    print("Attachments: " + meeting.attachments.len());
    let note = meeting.create_note("Who Spoke");
    for name in meeting.transcript.speakers {
        note.append("- " + name + "\n");
    }
}

Something wrong, out of date or missing on this page? Email support@samuraiscribe.com or ask in the Discord community.