# 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.

- Page: https://samuraiscribe.com/docs/scripting/cookbook/
- Checked with Samurai Scribe 0.65.0

Each recipe on this page is a complete [script](https://samuraiscribe.com/docs/glossary/#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](https://samuraiscribe.com/docs/scripting/#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](https://samuraiscribe.com/docs/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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
// 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.

```rhai
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**.

```rhai
// 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");
    }
}
```
