A macOS app for writing daily standups. Log what you worked on, track blockers and gratitude items, pull in your calendar and GitHub pull requests, and generate a formatted standup message with one keystroke.
Standup Buddy keeps a running log of work entries organized into three categories:
- Standup β tasks worked on each day; automatically split into Previous (last workday) and Today sections when generating
- Blocker β open items that stay in the output until manually closed
- Gratitude/Joy/Others β open items similar to blockers
The main window has three tabs:
- Entries β add, edit, and complete your log entries
- Song of the Day β log a song for today and (optionally) archive it to an Apple Music playlist β see Song of the Day
- Quick Preview β a read-only look at your current sections
The window toolbar holds three actions:
- Import from Calendar β pull today's calendar events in as entries (see Calendars)
- Generate (
ββ§G) β open the standup output window - Settings (gear) β open Settings
When you generate a standup (ββ§G), the app produces a Markdown-formatted message and opens it in the Standup Output window:
*Previous:*
* Fixed the login bug
*Today:*
* Reviewing the auth PR
*Blockers:*
* None
*Open Pull Requests:*
* My Repo: [Fix login flow](https://github.com/...)
*Gratitude/Joy/Others:*
* Great team retro today
The output window has two tabs:
- Preview β the rendered result, with clickable links
- Text β the editable Markdown source. A formatting toolbar lets you apply bold, italic, underline,
strikethrough, links, inline code, and code blocks; insert an emoji (via the macOS character palette); and insert any built-in or custom replacement token. Underline is disabled when the Slack flavor is selected, since Slack has no underline.
Edits you make in the Text tab are reflected in the Preview. When you're happy with it, click Copy to Clipboard. The current Markdown flavor is shown in the top-right corner of the window.
Standup Buddy can emit two Markdown dialects, selected under Settings β General β Markdown Engine:
- Slack mrkdwn (default) β single-asterisk
*bold*, underscore_italics_, single-tilde~strikethrough~, and no underline. Links use[text](url), which the Slack message composer auto-links. - GitHub-Flavored Markdown β the familiar
**bold**,*italics*,~~strikethrough~~, and<u>underline</u>.
The chosen flavor drives the section headers (*Today:* vs **Today:**), the editor toolbar, and how the preview is rendered, so the formatting buttons always insert the tokens that match where you'll paste the result.
Entry text can include special tokens that are expanded at generate time:
| Token | Expands to |
|---|---|
{{dad_joke}} |
A random dad joke from icanhazdadjoke.com. |
{{today}} |
The current workday name (e.g. Friday). Accepts an optional strftime format: {{today('%A')}}. |
{{format_date}} |
A synonym for {{today}}. |
{{previous}} |
The previous workday name (e.g. Friday). Accepts an optional strftime format: {{previous('%Y-%m-%d')}}. |
{{yesterday}} |
A synonym for {{previous}}. |
{{fun_fact}} |
A random fun fact from uselessfacts.jsph.pl. |
{{affirmation}} |
A random positive affirmation from affirmations.dev. |
{{emoji_of_day}} |
A consistent emoji for today's date β changes each day, the same for everyone. |
{{entry_date}} |
The date of the entry the token appears in (e.g. 2025-06-02). Accepts an optional strftime format: {{entry_date('%A')}}. |
Tokens use double curly braces (mustache/handlebars-style) so a single brace inside a pasted code snippet doesn't get mistaken for a replacement. Each built-in token can be toggled on or off, and custom regex-based replacements can be configured β see Settings below.
The Song of the Day tab lets you log a song for today and (optionally) archive it to an Apple Music playlist. When a song exists for today, generated standups get an extra section:
*Song of the Day:*
* π΅ [Mono Puff - Creepy](https://www.youtube.com/watch?v=cng85fJjkNg) π΅ - Side project from John F. of TMBG - [Playlist](https://music.apple.com/...)
If no song is logged for today, the section is skipped entirely β no * None placeholder, and it doesn't count as an "empty section" for the Hide empty sections setting.
Adding a song:
- Enter an Artist and Song title.
- Click Search Apple Music to find a catalog match (requires an Apple Music subscription and authorization β see Music). Picking a result fills in the Apple Music link and canonicalizes the artist/title spelling. Editing the artist or title afterward clears the match.
- Click Search YouTube to open a YouTube search for the song in your browser, then paste the video URL into the YouTube URL field.
- Add optional Notes, then click Add. If the song matched on Apple Music and an archive playlist is configured, it's added to the playlist automatically.
Each row in the list shows the date, badges for what's attached (YouTube, Apple Music, In Playlist), and any notes. Double-click a row (or its Edit button) to edit it in place β the date, artist, title, links, and notes are all editable, and you can re-run the Apple Music/YouTube searches. Right-click a row for more actions:
- Edit / Delete
- Repeat Today β moves the song's date to today, a quick way to feature the same song again
- Add to Playlist β retries the playlist add for a matched song that isn't in the playlist yet (e.g. after fixing the archive playlist setting)
- Open in Apple Music / Open on YouTube
Saving a song never depends on the playlist add succeeding β if it fails (no playlist configured, no network, playlist not found), a dismissible warning banner explains why, and you can retry from the row's context menu.
The standup output line is generated from a customizable template β see Standup Output under Music settings. The template supports {{song_link}} (YouTube URL if set, else the Apple Music URL, else plain text), {{artist}}, {{title}}, {{notes}}, and {{playlist_link}}, plus the standard text replacement tokens like {{format_date('%A')}}. Empty tokens and their surrounding - separators are dropped automatically, so a song with no notes or no playlist link still renders cleanly.
Settings is organized into tabs.
- Markdown Engine β choose the Markdown flavor.
- Built-in Text Replacements β toggle each token from the table above. Tokens that hit an external API (
{{dad_joke}},{{fun_fact}},{{affirmation}}) can be disabled if you'd rather not make network calls; the date and emoji tokens require no network access.
On first launch Standup Buddy asks you to choose a folder for its single SQLite database file. Pick a folder inside iCloud Drive to sync between Macs, or any local folder for single-machine use.
You can move the database later under Settings β Database:
- Change Folderβ¦ β pick a new location. If the folder already contains a compatible Standup Buddy database, the app switches to it. If the folder is empty, you're asked whether to move your current data into it or start fresh with an empty database. The change is validated before it's applied, and your previous folder is left untouched if anything goes wrong.
- Show in Finder β reveal the current database file in Finder.
Automatic backups. Whenever Standup Buddy needs to upgrade the database to a new schema (after you install an app update) or replace it during a reset, it first writes a timestamped copy into a Backups subfolder of your data folder (e.g. Backups/standupbuddy-backup-20260624-093015-premigration.sqlite). Backups are never deleted automatically, so you can always restore an earlier copy by hand.
Opening a newer database. If the database in your folder was created by a newer version of Standup Buddy than the one you're running β for example, an older Mac syncing a file written by a newer one β the app won't risk opening it and reading against an unexpected schema. Instead it shows a dialog offering to Quit Standup Buddy (so you can update the app or move the file yourself), Choose Different Folderβ¦, or Start Fresh β which backs the existing file up before creating a new, empty database.
Settings β Sections controls which sections appear in generated standups and what they're called:
- Toggle any of Previous, Today, Blockers, Open Pull Requests, Gratitude/Joy/Others, and Song of the Day on or off. Disabled sections are hidden from the preview and excluded from the generated output. (Song of the Day is also skipped automatically on any day with no song logged, regardless of this toggle.)
- Give any section a custom header. Leave it blank to use the default. Headers support text replacements like
{{yesterday}}and{{format_date('%A')}}. - Hide empty sections β when enabled, any section with no content is omitted entirely from the generated standup instead of showing a
* Nonebullet. Off by default.
Settings β Replacements lets you define additional regex rules applied after the built-in tokens. Each rule has:
- Pattern β a regular expression matched against the entry text
- Template β the substitution string (supports capture group references like
$1) - Sort Order β controls the order rules are applied
Custom replacements also appear in the output editor's Insert Replacement menu.
To include your open pull requests in generated standups:
- Create a GitHub Personal Access Token with
reposcope (classic) orpull_requests:read(fine-grained). - Open Settings β GitHub and paste the token into the Personal Access Token field, then click Save.
- Add one or more repositories with +. Each repository requires an owner, a name (
owner/name), and a display name shown in the standup output.
The token is stored in the macOS Keychain; it is never written to disk or included in any file tracked by the project.
Standup Buddy can turn today's calendar events into standup entries.
- Open Settings β Calendars and click Grant Calendar Access (macOS will prompt for permission).
- Enable the calendars you want to draw from. Calendars are grouped by account; to add Google, Exchange, or other calendars, add the account in System Settings β Internet Accounts and it will appear here automatically.
- Back in the main window, click Import from Calendar in the toolbar. Pick the events you want and click Add Events as Entries β each becomes a Standup entry for today. Events you've declined are skipped.
Settings β Music configures the Song of the Day feature. Using it requires an Apple Music subscription.
- Apple Music β click Request Access to authorize Apple Music. Once authorized, pick your Archive playlist from a list of your editable library playlists. Choosing a playlist tries to fetch its public share link automatically (used as
{{playlist_link}}); if the playlist has never been shared, you'll see a note to share it manually in Music (Share Playlist β Copy Link) and paste the link into the field below. - Standup Output β the Playlist share URL (auto-filled when possible, or paste it yourself) and the Song line template used to render the generated line. Leave the template blank to use the default shown above.
Check for Updates⦠appears in the application menu, just after About Standup Buddy. It compares the running version against the latest published GitHub release and tells you whether you're up to date or offers a View Release button to open the release page. The check is unauthenticated and requires only network access to the public repository.
Choose View β Show API Console Log to open the API Console, which logs all outbound requests (dad jokes, fun facts, affirmations, GitHub, and update checks) and their responses. It's handy for debugging a GitHub token or network issue.
- macOS 26 (Tahoe) or later
- An Apple Music subscription (only needed for the Song of the Day feature's catalog search and playlist archiving)
- macOS 26 (Tahoe) or later
- Xcode 26 or later
- XcodeGen (
brew install xcodegen)
The Xcode project is generated by XcodeGen from project.yml and is not committed. Always run xcodegen generate after pulling changes that touch the project configuration or after adding/removing source files. Swift Package dependencies (GRDB and MarkdownUI) are declared in project.yml and resolved by Xcode on first open.
Two files contain signing identity and are not committed to the repository. Copy each example and fill in your values before building.
cp project.yml.example project.ymlOpen project.yml and replace the two placeholders:
| Placeholder | Where to find it |
|---|---|
XXXXXXXXXX (Team ID) |
Apple Developer β Account β Membership β the 10-character string next to your name |
Your Provisioning Profile Name |
The name shown in Xcode β Settings β Accounts β Manage Certificates, or in the Apple Developer portal under Profiles |
Debug builds are team-signed (CODE_SIGN_STYLE: Automatic, CODE_SIGN_IDENTITY: "Apple Development") rather than ad-hoc, because MusicKit's automatic developer token β needed by the Song of the Day feature β only works in a build signed with a real team. A free Apple ID's personal team is enough to build and run; the MusicKit-specific features additionally need a paid Apple Developer Program membership with the MusicKit App ID service enabled (see below).
cp scripts/exportOptions.plist.example scripts/exportOptions.plistOpen scripts/exportOptions.plist and replace the XXXXXXXXXX placeholders with your Team ID and fill in your certificate and provisioning profile names. This file is only needed when running scripts/release.sh.
The Song of the Day feature needs MusicKit enabled on the app's App ID before catalog search or playlist adds will work:
- In the Apple Developer portal, enable the MusicKit App Service on the
house.gourley.StandupBuddyApp ID. - Regenerate and re-download the provisioning profile if the portal prompts you to.
- At runtime, the signed-in user needs an Apple Music subscription and to authorize access in Settings β Music (see Music settings).
MusicKit's developer token is derived automatically from the app's code signature β there's no API key to configure. MusicService.swift (Sources/Services/MusicService.swift) is the only file that imports MusicKit; playlist listing and adding songs go through the Apple Music REST API (MusicDataRequest) rather than MusicLibraryRequest/MusicLibrary.add(_:to:), since library-scoped IDs from the local request API aren't accepted by the REST tracks endpoint on macOS.
- Complete the first-time setup above.
- Generate the Xcode project:
xcodegen generate - Open the generated project:
open StandupBuddy.xcodeproj - Build and run with
βR.
xcodebuild test -project StandupBuddy.xcodeproj -scheme StandupBuddy -destination 'platform=macOS'
Or run the test suite directly from Xcode with βU. Tests live in Tests/ and cover the generator, text-replacement engine, workday calculation, update checker, database migration backups and safety guards, Song of the Day line rendering, and supporting helpers. MusicService itself is untested (a thin MusicKit/REST wrapper requiring real tokens and entitlements) β its decision logic lives in SongLineRenderer and AppModel, which are tested.
Check for Updates⦠compares the running version against the latest GitHub release. To exercise the "update available" path without bumping the version or publishing a release, set an environment variable that overrides the version the app reports for itself. This override only takes effect in DEBUG builds and is compiled out of release builds.
- In Xcode, choose Product β Scheme β Edit Schemeβ¦ (
β<). - Select Run β Arguments.
- Under Environment Variables, add
UPDATE_CHECK_CURRENT_VERSIONwith a value older than the latest release (e.g.1.0.0). - Run the app and choose Check for Updatesβ¦ β it will report an update and the View Release button opens the GitHub release page.
Toggle the variable's checkbox to flip between the "update available" and "up to date" results without rebuilding. Remove or uncheck it to test against the real bundle version. The check hits the public releases/latest endpoint, so it requires the bratta/StandupBuddy repository to be public (or network access to it).
Releases are produced by scripts/release.sh (build, sign, notarize, and package the DMG) together with the helpers in scripts/release/. The versioned MARKETING_VERSION / CURRENT_PROJECT_VERSION live in the gitignored project.yml, so a version bump produces no committable change β a release is a tag on main plus a published GitHub release. The notarization step requires a keychain profile named StandupBuddy.
