---
name: personalized-event-recap-emails
description: "Turn event session recordings and an attendee list with interest tags into recap email drafts, one per attendee, linking moments checked in Vivu. Use after a webinar series or conference."
---Personalized event recap emails with Vivu
This skill turns an event's session recordings and an attendee export with interest tags into one recap email draft per attendee. Claude reads the attendee list, turns each interest tag into a standing search (plus a spoken fallback), prices the job against the user's Vivu plan, indexes the recordings in Vivu, finds the moments for each tag, checks every moment against the video, maps the checked moments to each attendee by tag, and creates Gmail drafts that link to the user's own hosted recordings at the right second.
The value is in where each link lands. Event recordings name every topic several times: in the opening agenda slide, in a cold open, in passing. A transcript search finds all of those, and a link that lands there shows the host reading the agenda. This skill searches for what the product screen shows when a session covers the topic, falls back to speech only for talks without a demo, and checks each moment with frames from the video before it goes into an email, so a link opens on the demo or the explanation. Searches run once per interest tag, not once per attendee, so the search cost stays flat as the guest list grows.
When to use
Use when someone asks to send personalized post event recaps, follow up attendees with the sessions they care about, "send everyone the parts of the webinar they signed up for", or turn a conference's recordings into tailored follow up emails. The skill fits events with several sessions and an attendee list that records what each person is interested in (a registration question, CRM interest fields, or session check ins).
Not for: one recap for everyone (summarize the sessions and send one email), a single short webinar where every attendee saw the same thing (use Vivu search directly and link the best moments), or sales call follow ups to one account (those need the call transcript, not a tag search).
Working principles
- Report measured numbers, not estimates. When a number is an estimate, say so.
- Nothing is "verified" until it has been checked against the source video. Search results stay candidates until triaged, and only verified moments go into an email.
- Stop and tell the user when a required capability or tool is missing. Do not guess around it.
- Ask the user before anything that is expensive to redo or that acts on their behalf: indexing (it uses plan minutes), the query wording, and every Gmail draft.
- Search per interest tag, then map to people. A search per attendee multiplies credits by the guest list for the same moments.
- Links in emails point to the user's own hosted recording with a time parameter. A Vivu result page link expires after four hours, so it only appears in the live reply.
What you need before starting
The skill runs in Claude Code on the computer that holds the recordings (a terminal or the Code tab of Claude Desktop), because it needs a shell and local files. It runs once per event; nothing is scheduled. Check each item at the start of the run and tell the user plainly what is missing before doing anything else.
| Requirement | Why | How to check |
|---|---|---|
| Vivu connector with write access | create a project, open its upload page, search | vivu_get_account shows can_create_projects: true (tool names may carry a server prefix). "has not granted vivu.write" means the connection is read only: reconnect Vivu and allow write access. No Vivu tools at all: add the Vivu connector (https://mcp.vivu.ai/mcp) and stop |
| Attendee export as a CSV file | who gets an email and which topics each person asked for | open the file and list its columns; it needs an email column and at least one interest column |
| The session recordings as local video files | Vivu indexes files; ffmpeg reads frames from the same files for checking | ffprobe -v error -show_entries format=duration -of csv=p=0 FILE on each file |
| The public or on demand link for each session | the link the email uses, since the user's own hosting does not expire | open one link and confirm it plays and accepts a start time |
| A shell with ffmpeg and ffprobe, on the computer that holds the files | measure minutes, extract frames for checking | ffmpeg -version |
| Gmail connector that can create drafts | Step 7 | the Gmail tools are listed and a read call (list drafts or labels) works |
| Only when the recordings exist only on the user's own YouTube channel: yt-dlp and node on a machine with a residential IP | YouTube blocks downloads from cloud and datacenter IPs | yt-dlp --version, node --version, then the one video test download in Step 4 |
Inputs to collect
Ask for anything missing, most important first.
- The attendee export (required). Which column is the email, the first name, the interest tags, and sessions attended.
- The recordings folder and a table of session title and hosted link per file (required).
- The interest tags to cover and the topic behind each one. Default: every tag that at least one attendee picked, worded by Claude from the tag label and confirmed by the user in Step 2.
- Moments per email. Default: 3.
- Event name, sender name, and one line of tone guidance for the email. Default: short plain text, no images.
- Whether to point people at sessions they already attended. Default: yes, but list sessions they missed first.
Files and state
Keep everything in one working folder on the computer that holds the recordings:
event-recap/
attendees.csv the user's export, unchanged
sessions.csv file, session_title, source_url, duration_s, video_id
queries.json tag -> query wording, mode, maximum_results, job_id
results/ the raw search result for each tag, one JSON file each
frames/ frames extracted to check each moment
recap_moments.csv one row per checked moment (verified or false positive)
recap_map.csv one row per attendee: tags and chosen moment ids
emails/ one rendered .txt per attendee, the text that goes into Gmail
state.json project_id, uploaded video_ids, searched tags, moment ids, drafted emails and their draft ids
A rerun reads state.json first and skips what is done: files already in the project, tags already searched, attendees who already have a draft. Each attendee gets at most one draft per event, keyed by email address, so an interrupted run never produces a second draft.
Step 1: Check the Vivu connector and the setup
Goal: every row of What you need is present, or the user knows exactly what is missing.
- Call vivu_get_account. If the Vivu tools are missing, tell the user to add the Vivu connector in Claude (https://mcp.vivu.ai/mcp) and stop. The account must show can_create_projects: true. If a later write call fails with "has not granted vivu.write", ask the user to reconnect Vivu and allow write access, then retry.
- Run ffmpeg -version and ffprobe -version. Without them nothing can be measured or checked; stop and say so.
- Check that the Gmail tools are listed and one read call works. If Gmail is missing, the skill can still write emails/*.txt and recap_map.csv for the user's own mail tool; say that this is the fallback before continuing.
- Open the attendee CSV and print the column names and the row count.
Done when vivu_get_account shows can_create_projects: true, ffmpeg answers, and the user has confirmed which columns hold email, first name and interest tags.
Step 2: Turn interest tags into standing searches
Goal: two searches per tag, worded the way an attendee would describe the moment.
- Count attendees per tag. Tags with nobody on them are dropped. Attendees with no tag get no personalized email; list them so the user can send them the general recap.
- For each tag, write a screen query: what the topic looks like on screen when a session covers it, in the attendee's words, naming the product area. Example: the tag "LLM costs" becomes "the product screen showing token usage and spend for OpenAI or Anthropic models, such as a cost dashboard broken down by model".
- For each tag, also write a spoken query, used only as a fallback in Step 5: what the speaker explains. Example: "explaining how a team tracks its usage and spending on OpenAI or Anthropic models". Sessions open with an agenda that names every topic, so a spoken query lands on that agenda as often as on the content; the screen query does not, because the agenda is a slide with a speaker, not the product. Talks without a demo (a customer story, a research summary) only show up for the spoken query. This is the modality flip, from shown to said.
- Show the user the table of tag, attendee count, screen query, spoken query, and mode. Wait for approval of the wording.
Done when the user has approved both queries for every tag and queries.json holds them.
Step 3: Size the job and get approval
Goal: the user sees the index minutes and search credits before anything is uploaded.
- Measure every recording: ffprobe -v error -show_entries format=duration -of csv=p=0 FILE. Sum to minutes.
- Count searches: one precise screen search per tag, plus one precise spoken search for each tag that may need the fallback (budget for all of them). A precise search uses 5 credits in total; a fast search uses 1 credit. Label the total as an estimate, since a reworded query costs another search. The count depends on the number of tags, not the number of attendees; searching per attendee would multiply the same searches by the guest list.
- Call vivu_get_usage for the plan and what remains this month. The plans: Free is $0 a month with 20 indexing minutes a month and 50 search credits a month; Premium is $30 a month with 180 indexing minutes a month and 500 search credits a month.
- Show one table:
| This event | Remaining this month | Fits | |
|---|---|---|---|
| Index minutes | measured sum | from vivu_get_usage | yes or no |
| Search credits (estimate) | tags x 2 x 5 | from vivu_get_usage | yes or no |
If it does not fit, offer these levers in order: index only the sessions whose agenda or title covers at least one tag somebody picked (the keynote usually stays); merge tags that mean the same thing; skip the spoken fallback for tags with very few attendees; split the event across two months; move to a larger plan. Never drop a session the user asked for without saying so.
If vivu_get_usage returns no remaining figures (some admin or team accounts return null), show the needed minutes and credits anyway and ask the user to confirm the allowance.
Done when the user approves the session list and the cost table.
Step 4: Upload and index
Goal: every approved session is ready in one Vivu project.
If the only copy of a recording is on the user's own public YouTube channel, download it first on the residential IP machine, one video as a test before the batch. 720p keeps slide and UI text readable at a fraction of the 1080p size:
yt-dlp --js-runtimes node --restrict-filenames \
-f "bv*[height<=720]+ba/b[height<=720]" --merge-output-format mp4 \
--match-filter "duration<=MAX_SECONDS" \
--download-archive event-recap/archive.txt \
-P "temp:SCRATCH_DIR" -P "home:event-recap/recordings" \
-o "%(id)s_%(title).60B.%(ext)s" --retries 5 \
"https://www.youtube.com/watch?v=VIDEO_ID"
VIDEO_ID is the 11 character ID in the session's link, MAX_SECONDS the longest session in seconds, SCRATCH_DIR any folder outside recordings/. If yt-dlp stops with "HTTP Error 403: Forbidden", retry once; if it persists, adding --extractor-args "youtube:player_client=mweb" can work but may only offer 360p, so check the resolution with ffprobe and prefer the platform's own download when one exists. The youtube-competitor-watch skill (https://vivu.ai/skills/youtube-competitor-watch) covers channel verification and download troubleshooting in more depth. Recordings on a webinar platform (Zoom, Vimeo, Wistia, ON24) are downloaded from the platform's own download button instead.
- Call vivu_list_projects. Reuse a project for this event if one exists. Otherwise call vivu_create_project with a name like "Event recap EVENT_NAME". Pass visibility "private" when the recordings include customer speakers or anything not yet public; the default is "organization", which every member of the Vivu workspace can see. Record the project_id in state.json.
- Call vivu_open_upload_page with the project_id right before the upload. The link expires in 180 seconds, so request it only when the user or the browser tool is ready, and never paste it into a message or file.
- Give the link to the user to open in their own browser and select the files, or open it with a browser tool that can attach local files. Claude in Chrome accepts at most 10 MB per upload call; files above that go through the user's browser or the Vivu web app. Never split or recompress a recording to fit.
- Vivu replaces spaces and brackets in file names with underscores. Match videos back to sessions.csv by a unique part of the name (a session number or ID), not the full name.
- Poll vivu_list_videos every 30 seconds or so until every video shows status ready. Write each video_id into sessions.csv and state.json.
The upload through the user's own browser was not exercised in our test run; the test recordings reached Vivu through a different upload path. The indexing wait was: four recordings, 30.3 minutes in total, were all ready about 10 minutes after the upload started. Plan for the wait on a real event and poll instead of guessing.
Done when vivu_list_videos shows every approved session as ready and sessions.csv has a video_id on each row.
Step 5: Search each tag and check every moment
Goal: a list of verified moments per tag, with every false positive counted and kept out of the emails.
The standing queries, one row per output field. The wording below is the wording from our test run; replace the topic with each tag's topic in the attendee's words.
| Field | Query | Mode | maximum_results |
|---|---|---|---|
| Screen moment, LLM costs tag | the product screen showing token usage and spend for OpenAI or Anthropic models, such as a cost dashboard broken down by model | precise | 10 |
| Screen moment, feature flags tag | the feature flags screen in the product, where targeting rules and rollout steps for a flag are configured | precise | 10 |
| Screen moment, security tag | the security product screen listing detected threats, detection rules, or cloud misconfigurations | precise | 10 |
| Spoken fallback, any tag | explaining how a team tracks its usage and spending on OpenAI or Anthropic models | precise | 10 |
All searches are precise because every moment needs a timestamp, and only precise returns one; fast returns the whole file with an empty reason. maximum_results is 10 because one topic rarely has more than a handful of moments in an event, and it is also the ceiling on what can be found: raise it to 20 for events with more than about 20 sessions.
- Run the screen query for each tag with vivu_search_videos (project_id, query, mode "precise", maximum_results 10). It returns a job_id. Call vivu_get_search_results until complete is true; each status call can wait up to 45 seconds, so a pending search is not a stalled one. Save the completed result as results/TAG.json. The result page link may go in the live reply; it expires after four hours, so it never goes into a file or an email.
- Check every hit before it counts. Find the local file for the hit's video_id in sessions.csv and extract three frames: at start_ms, at the middle, and half a second before end_ms.
ffmpeg -v error -ss SECONDS -i FILE -frames:v 1 -q:v 3 event-recap/frames/TAG_hN_POSITION.png
SECONDS is the time in seconds (start_ms divided by 1000, and so on), FILE the local recording, TAG_hN_POSITION a name such as security_h2_mid. Look at each frame. The start frame also decides where the email link lands (see the paragraph on linking below); a window can start on the previous topic. The hit is real when the frames show the topic being covered: the product screen, a demo, or a slide that explains it. It is a false positive when the frames show the session's opening agenda or a cold open that only names the topic, a similar looking screen from a different feature, or a montage of unrelated cards. 3. For a hit with no product screen (a talk), also read vivu_get_video_summary with include_segments true and start_ms and end_ms around the hit. The segment titles are chapter level and can sit beside the moment, so when the title disagrees with the frames, check the session's own subtitles or transcript before deciding. Never quote the reason text in an email; it paraphrases, and it can describe things the frames do not show. 4. Write each checked hit to recap_moments.csv: moment_id, tag, session_title, source_url with the time parameter, start_s, end_s, start_mmss, covers (one line written from the frames), verified_by, and status (verified or false_positive). 5. For each tag with no verified screen moment, run the spoken fallback query the same way. Drop any hit inside the session's opening agenda or cold open, and check the rest as above, including the start frame, because spoken windows in our test run sometimes began on the previous topic or the agenda. 6. Show the user, per tag: moments returned, real, false, and the tags left without a verified moment. If a tag's first page has false positives, reword it once in the attendee's words (name the product area, describe the screen) and rerun, and record that the wording was tuned.
Treat every screen hit as a candidate until the frames are checked. Matching on what is on screen is a weak Vivu capability: it can confuse a similar screen from another feature, or a montage card, with the topic (both happened in the worked example below). Speech matching is stronger but also matches the agenda.
Vivu returns a time range that contains the moment, not an exact frame. In our test run every real hit from the screen wordings started inside the part of the session about that topic, but some real hits from the spoken wordings started a few seconds early, on the end of the previous topic's demo or on the opening agenda slide. The frames just after those early starts showed the host talking to camera or the same agenda slide before anything about the topic appeared on screen.
So look at the start frame from item 2 above: if it shows the topic, link at start_s; if it shows another topic or the agenda, step forward through the window in 2 second frames (the ffmpeg command above at start_s + 2, start_s + 4, and so on) and link at the first frame that shows this topic, and record that second as start_s in recap_moments.csv. While stepping, a frame of the host talking to camera or of the agenda again does not count as showing the topic; keep going to the first title card, slide or product screen about it.
Worked example from our test run
The event was four public recordings (30.3 minutes) of one SaaS vendor's monthly product update show, treated as the sessions of one event, with a synthetic attendee list; real attendee data only exists in a customer's CRM. Indexing used 30.3 minutes, and all four were ready about 10 minutes after the upload started. Truth came from the publisher's chapter markers checked against subtitles, written before any search ran; one security segment was added after the first search surfaced it.
The spoken wordings, one per tag, returned 18 moments: 11 real and 7 false. All but one of the false ones were an opening agenda slide or a cold open that only named the topic; the other was a percentage slider on a different product read as a feature flag rollout.
The screen wordings, after 1 rewording on the same corpus, returned 11 moments: 10 real and 1 false, a silent montage of release cards. LLM costs returned 3 moments, all 3 real; feature flags returned 2, both real; security returned 6, 5 real and 1 false. They missed 1 truth segment, a research summary given only in talk, which the spoken wording had found. That is why the spoken query stays as the fallback.
All searches were precise, 7 in total, an estimate of 35 credits for the whole test run. The mapping and the rendered emails in Step 6 ran on the synthetic list; Step 7 did not run.
Done when every tag has a screen search (and a fallback search where needed) and recap_moments.csv marks every returned hit as verified or false_positive, with the counts shown to the user.
Step 6: Map moments to attendees and approve one sample
Goal: each attendee has up to three verified moments from their own tags, and the user has approved one fully rendered email.
- For each attendee, collect the verified moments for each of their tags. Order them: sessions the attendee did not attend first, then the rest; within that, the demo moment before the explanation when both exist. Drop repeats of the same session and second. Keep at most the moments per email chosen in Inputs (default 3).
- If an attendee's tags have fewer verified moments than that, send the real number. Never fill the gap with a moment from another tag or an unverified candidate. An attendee whose tags have no verified moment gets no personalized email; list them for the general recap.
- Write recap_map.csv, one row per attendee (SESSION_ID is the session's ID from sessions.csv; moment ids come from recap_moments.csv):
email,first_name,tags,moment_ids,attended_sessions,note
attendee01@example.com,Attendee01,llm_costs;feature_flags,m01;m04;m03,SESSION_ID,
- Render every email to emails/EMAIL.txt from this template. MM:SS comes from start_s. The link is the session's source_url plus the time parameter for that host (YouTube &t=SECONDS, Vimeo #t=SECONDSs, Wistia ?wtime=SECONDSs); never the Vivu result page link, which expires after four hours.
To: EMAIL
Subject: Your EVENT_NAME recap: TAG_LABELS
Hi FIRST_NAME,
You told us you wanted to hear about TAG_LABELS. Here are the moments worth your time:
- TAG_LABEL, "SESSION_TITLE" at MM:SS: COVERS
SOURCE_URL_WITH_TIME
SENDER_SIGNOFF
- Show the user recap_map.csv (first ten rows), the counts (attendees with an email, attendees routed to the general recap, moments per email), and one rendered email in full. Say which fields are inferred: COVERS is Claude's one line description of what the frames and summary show, not a quote. Wait for approval before Step 7.
Done when recap_map.csv and one emails/*.txt per attendee exist, and the user has approved the sample email and the mapping.
Step 7: Create the Gmail drafts
Goal: one Gmail draft per approved attendee, nothing sent.
- Creating drafts acts in the user's mailbox. Name the destination (the user's Gmail account, Drafts folder), repeat the count, and get an explicit yes for the batch after the sample in Step 6 was approved.
- For each attendee without a draft in state.json, create a draft with the Gmail connector's draft tool, using emails/EMAIL.txt for the recipient, subject and body. Record the returned draft id in state.json right after each call, so a rerun skips it.
- Do not send. The user reviews and sends from Gmail, or imports recap_map.csv into their email platform instead. Sending straight from the skill happens only if the user asks for it explicitly, and then only after one test send to the user's own address.
Step 7 was not exercised in our test run: the test rendered each email to emails/*.txt and created no Gmail drafts, so the draft tool's behavior with this template is untested.
Done when state.json lists a draft id for every attendee in recap_map.csv and the user has been told the draft count and where to find them.
Compliance
- Speakers. Event recordings show speakers and often customer guests. Confirm with the user before Step 4 that speakers agreed to be recorded and that the recordings are already published or cleared for on demand viewing; the emails send people to those recordings.
- Recordings the user does not own. If a session is someone else's footage (a partner's talk hosted elsewhere), confirm the user may link to it and index it for internal analysis only, with no redistribution, before Step 4.
- Attendee data. The attendee export is personal data. It stays in the local working folder and in the user's own Gmail drafts; the skill never uploads it to Vivu. Before Step 7, confirm the user may email these people about this event (registration consent, unsubscribe link in their usual template) under the rules that apply to them.
- Minors. B2B events rarely involve minors. If any session features minors, do not index it without the guardian consent the user already holds for publishing it.
- Where the videos go. Indexed recordings stay in the user's Vivu project until the user deletes them. Create the project as private for anything not public yet. The skill never calls vivu_delete_project or vivu_delete_video unless the user asks, and confirms first.
- The skill identifies topics, not people. It does no face, logo or speaker identification.
Known failure modes
| Symptom | Cause | Fix |
|---|---|---|
| (observed) a topic search returns the session's opening agenda or a cold open | every session names its topics up front, on a slide and out loud, and a spoken query matches the name | search for the product screen first; in triage, drop any hit inside the opening agenda or cold open |
| (observed) a spoken hit's window starts on the previous topic's demo or the agenda slide | the time range contains the moment but can start before it | check the start frame and move the link to the first frame that shows the topic |
| (observed) a screen from a different feature matches (a requests slider read as a feature flag rollout) | the screens look alike and the reason text follows the query | name the product area in the query ("the feature flags screen"); judge by the frames |
| (observed) a hit lands on a silent montage of release cards | one card in the montage touches the topic in passing; the reason named a tab the frames do not show | count it as a false positive; never copy the reason into an email |
| (observed) the summary section around a real hit has another title ("Metrics Volume Management Page") | summary sections are chapter level and can sit beside the moment | decide by frames and the session's subtitles; use the summary as a second signal only |
| (observed) a talk with no product screen is missing from the screen results | the screen query needs the product on screen | run the spoken fallback for tags left without a verified moment |
| (observed) "HTTP Error 403: Forbidden" from yt-dlp on a residential IP | YouTube refused the default client | retry once; the mweb player client worked but offered only 360p; prefer the platform's own download |
| "has not granted vivu.write" | Vivu connected read only | the user reconnects Vivu and allows write access |
| upload page asks to sign in or shows an error | the one time upload link expires after 180 seconds | request a new link right before opening it |
| a file is rejected by the browser upload tool | the file is above the tool's limit (Claude in Chrome takes up to 10 MB per call) | the user opens the upload link in their own browser or adds the file in the Vivu web app; never split or recompress it |
| a video in Vivu does not match a row in sessions.csv | Vivu replaced spaces and brackets in the file name with underscores | match on a unique ID or session number inside the name |
| an email link opens an expired page | a Vivu result page link was used; it expires after four hours | rebuild the link from sessions.csv source_url plus the host's time parameter |
| a tag has no verified moment | the event never covered the topic, or the query wording missed it (an empty result does not prove the footage has no such moment) | reword once in the attendee's words; if still empty, route those attendees to the general recap and tell the user |
| the plan runs out of index minutes or credits mid run | the event is larger than the remaining allowance | apply the Step 3 levers in order; nothing is dropped without telling the user |
| Gmail tools are missing or a draft call fails | Gmail connector not added or not authorized | stop Step 7, give the user emails/*.txt and recap_map.csv for their own mail tool |