A bookmarklet that pulls the transcript off a Zoom notes page and downloads it as a clean markdown file.
One click. No extension, no login, no data leaves your browser.
A transcript.md file formatted like this:
# Transcript
**Garry Creath** [12:02:07]
And do you need to communicate anything with Next Step?
**Elliot Boney** [12:02:19]
Not yet. Waiting on the pipeline numbers first.Speaker names, timestamps, and message text. Ready to drop into notes, feed to an LLM, or diff against a summary.
Zoom renders each transcript turn as a row: a header with the speaker name and a timestamp, followed by the message text.
The script finds every element on the page whose entire text is a bare timestamp, then walks up from there to find the row it belongs to. Speaker name comes from the row's aria-label, with a fallback that strips the timestamp out of the header text. Everything in the row that isn't the header gets treated as message content.
Anchoring on timestamps matters. Zoom's class names are generated hashes that change on every deploy, so anything that targets them breaks within weeks. Timestamps are stable because they're content, not styling.
Rows with no body text get skipped, which filters out stray timestamps elsewhere on the page. Turns are assembled into markdown and handed to the browser as a Blob download.
- Create a new bookmark in your browser. Name it whatever you want.
- Paste this as the URL:
javascript:(function(){var R=/^\d{1,2}:\d{2}(:\d{2})?$/;var S=[].slice.call(document.querySelectorAll('*')).filter(function(e){return e.children.length===0&&R.test(e.textContent.trim())});var seen=new Set();var T=[];S.forEach(function(s){var h=s.parentElement;if(!h)return;var r=h.parentElement;if(!r||seen.has(r))return;seen.add(r);var tm=s.textContent.trim();var n='';var ne=h.querySelector('span[aria-label]');if(ne)n=ne.getAttribute('aria-label').trim();if(!n)n=h.textContent.trim().replace(tm,'').trim();var m=[];[].slice.call(r.children).forEach(function(c){if(c===h)return;var x=c.textContent.trim();if(x)m.push(x)});if(!m.length)return;T.push('**'+n+'**'+(tm?' ['+tm+']':'')+'\n'+m.join(' '))});if(!T.length){alert('No transcript turns found. Make sure the transcript is fully loaded on screen.');return}var md='# Transcript\n\n'+T.join('\n\n')+'\n';var b=new Blob([md],{type:'text/markdown'});var u=URL.createObjectURL(b);var d=document.createElement('a');d.href=u;d.download='transcript.md';document.body.appendChild(d);d.click();d.remove();URL.revokeObjectURL(u)})();
Paste it exactly. Don't let anything re-encode the spaces or quotes. A %20 where a space belongs will break the script on parse.
- Open your Zoom notes page and find the transcript.
- Scroll through the entire transcript, top to bottom. This is the step people skip.
- Click the bookmark.
transcript.mdlands in your downloads.
Zoom virtualizes long transcripts. Turns that haven't been scrolled into view don't exist in the page yet, so there's nothing to scrape. Scroll all the way down, then all the way back up, then click.
If your output is missing the first half of the meeting, this is why.
"No transcript turns found." The transcript isn't loaded, or you clicked it on the wrong page. Scroll the transcript into view and try again.
Output is missing turns. Virtualization. See above.
A speaker name looks wrong or shows up as a blank.
Zoom occasionally drops the aria-label on a row. The fallback should catch it. If it doesn't, the name is probably rendering somewhere unexpected on that page.
Nothing happens at all, not even the alert.
The bookmarklet got mangled on paste. Re-copy it and check that there are no %20 or %22 sequences in the bookmark URL.
- Built against the Zoom notes page layout. Zoom changes their DOM without warning, and a big enough change will break this.
- Reads only what's rendered in the page. It has no API access and can't pull anything you can't already see.
- Long messages that Zoom splits across multiple elements get joined with a space.