Skip to content

Commit c0c147c

Browse files
neilsarkarclaude
andauthored
Perfect Meetings: setup states, Sync button, Notion styling, and simpler workflows (#123)
Fixes from the 2026-10-01 QA session on the Perfect Meetings template. ## What changes **Block (`main_ui`, formerly `nextMeeting`)** - Setup states come from data already in the app (no stored flag, no new status DBs): - never run: a **Sync calendar** button - waiting: "Waiting for flow to start" - syncing: "Reading latest calendar events" - researching: "Researching companies and people (k of n meetings ready)" - failed: the error, with Retry - ready: the calendar - The **Sync calendar** button creates a Workflow runs row. Clicking twice doesn't create two rows. - Once populated, the block never goes back to the loading or error panels. Later errors appear as an inline notice above the last good data. - Runs or research with no change for 20 minutes are treated as stuck, so the button comes back. - Every empty state has a quiet **Sync now** button, which shows the same stages inline. - Person and company cards show a loading state while their meeting is being researched, instead of "Role not found yet". - The block opens on **Today**; Next meeting is one click away. - Restyled with the NDS tokens from `@notionhq/custom-blocks`: type, color, radius, spacing, icons, and a skeleton loader. - The block uses 4 live queries, down from 8 (one per data source), and works out every view in memory. This fixes the 429s from `queryCollection` during research: the SDK re-queries every query on a data source each time that source changes. **Workflows** - Display names are now **Sync calendar**, **Research companies and people** and **Refresh today's research**. The keys are unchanged. - All three manual runs take no input (`-d '{}'`). The manual modes and params are removed, which also makes the `eventStartTime`/`mode` validation unnecessary. - The calendar event triggers (created, updated, cancelled) are removed. Ingest runs hourly, when a Workflow runs row is added, or by hand. The hourly sync now marks cancelled, declined or missing events as Cancelled. - Run with no input, **Research companies and people** works through a backlog: - Records never researched go first. - A new **Researched at** date on People and Companies records each attempt, so the backlog keeps moving. - Failed meetings are retried after a 6-hour cooldown. - Each run handles at most 10 meetings. **APP.md and docs** - APP.md removes: - "How it works" - the linked databases for meetings and Workflow runs - the filler copy - "How it works" explanations move into property descriptions. - The toggle is renamed **Debug Data**, with one-line labels. - The README and `catalog.json` now use one app name ("Perfect Meetings") and describe the new behavior. ## Board tasks - Differentiate "still loading" vs "missing" - Template empty state nicer than an empty box - Make the block more Notion-like - Remove "How it works" - Simplify manual trigger - Validate `eventStartTime` against `mode` (no longer applies) - Template side of "Calendar ingest event created/updated/cancelled doesn't save via Set Up": the unsupported triggers are removed. The Set Up save bug itself is a platform issue. ## Verification - In `apps/templates/perfect-meetings`: - `npm run check` passes. - `npm test` passes (146 tests). - `npm run build` passes. - Repo root: - `catalog:validate`, `agents:check`, `agents:test` and `lint:markdown` pass. - `projects:typecheck` fails only in `apps/templates/apps-default`, which this PR doesn't touch. - Deployed to a dev test workspace and checked by hand while iterating: - fresh install - the Sync button and its stages - empty states - the 429 fix ## Notes - Renaming the block changes its slash command from `/next-meeting` to `/perfect-meetings`. The capability key is `main_ui`. - The SDK refetches every query on a data source with no debounce. This is worth raising with the custom blocks SDK team. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 80ff4db commit c0c147c

32 files changed

Lines changed: 3999 additions & 1404 deletions
Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,9 @@
1-
Reads your external meetings, researches companies and participants, and provides prep materials.
2-
3-
<database inline="true" data-source-url="{{meetings-next-view}}">Next meeting</database>
4-
5-
Switch to **Today** to see the day's meetings on a calendar, with the current or next one highlighted. Click a meeting to see its company and attendee cards.
6-
7-
<database inline="true" data-source-url="{{meetings-upcoming}}">Upcoming meetings</database>
8-
9-
<database inline="true" data-source-url="{{sync-runs-recent}}">Workflow runs</database>
10-
11-
Add a new row to sync your calendar now.
12-
13-
---
14-
15-
## How it works
16-
17-
Perfect Meetings helps you prepare for meetings with people outside your
18-
company. It brings your Google Calendar meetings into Notion and writes a
19-
brief about the companies, attendees, recent Gmail conversations, and likely
20-
meeting objective.
21-
22-
Open a meeting to review its **Meeting prep** and take notes under **Notes**.
23-
Refreshing the prep leaves your notes untouched. Tick **Regenerate prep** to
24-
request a fresh brief or retry a failed one. Each person's **Confidence** shows how
25-
sure the researcher is of their name and role. Low means it matched only part
26-
of the name, such as a first name at the right company; their card shows
27-
"(low confidence)", and a later brief may correct them. Set it to High once
28-
you have checked a person to keep their name and role.
29-
30-
Each brief also updates a **Profile** at the top of each attendee's page in
31-
**People**: their role and responsibilities, meetings with them in the last 90
32-
days, and a summary of recent meetings and email. Write your own notes under
33-
**Notes** on that page; refreshes leave them untouched. The researcher also
34-
fills in their **LinkedIn**, **X**, **Instagram**, and **Personal site** when
35-
it finds them, and the meeting card links to them. It only looks for profiles
36-
that are empty, so correct a wrong one by replacing it.
37-
38-
Meetings update when calendar events change, with an hourly catch-up, and
39-
every catch-up is logged in **Workflow runs**. Briefs are prepared for new
40-
meetings and refreshed when attendees change. Today's briefs also refresh each
41-
morning at 7:45 am in the app's configured time zone. To refresh today's
42-
briefs now, manually run **Morning prep**, leaving the date empty. Meetings
43-
with only coworkers do not appear by default.
1+
<database inline="true" data-source-url="{{meetings-next-view}}">Today</database>
442

453
<details>
46-
<summary>App data</summary>
47-
<mention-page url="{{people-db}}">People</mention-page> — Look up outside attendees, their roles, and their profiles.
48-
<mention-page url="{{companies-db}}">Companies</mention-page> — Browse company summaries and websites.
49-
<mention-page url="{{meetings-db}}">Meetings</mention-page> — Switch to **Calendar** or **All meetings** to browse dates and past notes.
50-
<mention-page url="{{sync-runs-db}}">Workflow runs</mention-page> — Every calendar catch-up, with its status and any error.
4+
<summary>Debug Data</summary>
5+
<mention-page url="{{sync-runs-db}}">Workflow runs</mention-page> — Run history of background tasks
6+
<mention-page url="{{meetings-db}}">Meetings</mention-page> — Calendar events synced with Notion
7+
<mention-page url="{{companies-db}}">Companies</mention-page> — Companies you meet with
8+
<mention-page url="{{people-db}}">People</mention-page> — People you meet with
519
</details>

‎apps/templates/perfect-meetings/README.md‎

Lines changed: 147 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,132 @@ Notion pages with a short research brief.
99

1010
## What it creates
1111

12-
| Kind | Name | Purpose |
13-
| ------------ | ------------------ | --------------------------------------------------------------------------- |
14-
| Page | APP.md | Home page with the next-meeting card, upcoming meetings, and usage guidance |
15-
| Database | Meetings | One row per external meeting; brief at the top of the page, notes below |
16-
| Database | People | One row per outside attendee, keyed by email |
17-
| Database | Companies | One row per outside company, keyed by email domain |
18-
| Database | Workflow runs | One row per calendar ingest run; add a row to run a catch-up now |
19-
| Custom agent | Meeting researcher | Web research plus email summary, returned as JSON |
20-
| Custom view | Next meeting | Company and people cards for the next meeting, plus a Today calendar view |
12+
| Kind | Name | Purpose |
13+
| ------------ | ------------------ | ------------------------------------------------------------------------------------------------- |
14+
| Page | APP.md | Home page with the main UI block and a Debug Data toggle linking the databases |
15+
| Database | Meetings | One row per external meeting; brief at the top of the page, notes below |
16+
| Database | People | One row per outside attendee, keyed by email |
17+
| Database | Companies | One row per outside company, keyed by email domain |
18+
| Database | Workflow runs | One row per calendar ingest run; add a row to run a catch-up now (linked under Debug Data) |
19+
| Custom agent | Meeting researcher | Web research plus email summary, returned as JSON |
20+
| Custom block | main_ui | Setup progress, then a Today view (the default) and company and people cards for the next meeting |
2121

2222
## Workflows
2323

24-
- `calendarIngest`: calendar event created, updated, or cancelled, plus an
25-
hourly backfill. Upserts Meetings by event ID, People by email, and
26-
Companies by domain. On first run it adds the two-way relations
27-
(Attendees, Companies, Company), which the SDK cannot declare yet. Each run
28-
is logged in Workflow runs as Pending, then Success or Failed; adding a row there
29-
runs a catch-up. A run that crashes or times out stays Pending, because the
30-
SDK has no hook for that yet.
31-
- `meetingPrep`: a Meetings row is created, its attendees change, or
32-
**Regenerate prep** is ticked. Searches Gmail (last 90 days, 10 threads per
33-
attendee), calls the researcher agent, and rewrites the **Meeting prep**
34-
section. Anything under **Notes** is left alone.
35-
- `morningPrep`: every day at 07:45 in `TIME_ZONE`, it refreshes the prep for
36-
that day's meetings.
24+
Each workflow keeps its key (used by `ntn workers exec`); the name is what
25+
Notion shows.
26+
27+
- **Sync calendar** (`calendarIngest`): runs hourly, when a row is added to
28+
Workflow runs, or by hand. It has no calendar event triggers, so changes
29+
show up within the hour. Each run scans 1 day back and 7 days ahead and
30+
upserts Meetings by event ID, People by email, and Companies by domain. A
31+
cancelled or declined event marks its meeting Cancelled, and so does an
32+
event that a complete scan no longer returns (deleted). On first run it
33+
adds the two-way relations (Attendees, Companies, Company), which the SDK
34+
cannot declare yet. Each run
35+
is logged in Workflow runs as Pending, then Success or Failed; adding a row
36+
there, or pressing **Sync calendar** in the block, runs a catch-up. A run
37+
that crashes or times out stays Pending, because the SDK has no hook for
38+
that yet. Its manual trigger takes no input and is always a catch-up.
39+
- **Research companies and people** (`meetingPrep`): a Meetings row is
40+
created, its attendees change, or **Regenerate prep** is ticked. Searches
41+
Gmail (last 90 days, 10 threads per attendee), calls the researcher agent,
42+
and rewrites the **Meeting prep** section. Anything under **Notes** is left
43+
alone. Its manual trigger takes no input and works through the research
44+
backlog (see below).
45+
- **Refresh today's research** (`morningPrep`): every day at 07:45 in
46+
`TIME_ZONE`, it refreshes the prep for that day's meetings. Its manual
47+
trigger takes no input and refreshes today.
48+
49+
### Research backlog
50+
51+
Research runs per meeting: one session covers a meeting's brief, its
52+
attendees, and their companies. A manual **Research companies and people** run
53+
therefore picks meetings that together cover everyone not yet researched
54+
(`src/workflows/lib/backlog.ts`). Among meetings that are not cancelled and
55+
started in the last 30 days:
56+
57+
- **First**, meetings that include a person or company research has never
58+
reached, and that no earlier pick already covers. Research stamps
59+
**Researched at** on every attendee and company when a session returns,
60+
even if it found nothing, so each record is researched once. (Records
61+
researched before that property existed count through Role, Confidence, or
62+
Summary.) A failed session stamps nothing, so its records stay in the
63+
backlog.
64+
- **Then**, upcoming meetings whose prep never finished: Prep status empty,
65+
Queued, or Failed.
66+
67+
Skipped: meetings without outside attendees; meetings Researching now, unless
68+
nothing has changed for 20 minutes, when they count as stuck and are retried;
69+
and Failed meetings whose last attempt was less than 6 hours ago
70+
(`FAILED_RETRY_MS`). That cooldown keeps a persistent failure from taking a
71+
slot on every run. Within each group, upcoming meetings go first, soonest
72+
first, then past meetings, most recent first. Each run preps at most 10
73+
meetings (`BACKLOG_MAX_MEETINGS`) and logs how many are left; run it again
74+
to continue.
75+
76+
## Block states
77+
78+
The block works out what to show from Workflow runs and Meetings; it stores
79+
no state of its own (`blocks/main_ui/state.ts`):
80+
81+
| State | When | Shows |
82+
| ----------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------- |
83+
| Never run | Workflow runs is empty | A **Sync calendar** button |
84+
| Waiting | The newest run (by Started) has no Status | "Waiting for flow to start" |
85+
| Syncing | The newest run is Pending | "Reading latest calendar events" |
86+
| Researching | The newest run succeeded and meetings are Queued/Researching | "Researching companies and people (k of n meetings ready)" |
87+
| Failed | The newest run failed | The run's Error and a **Retry sync** button |
88+
| Ready | The App is populated, or nothing above applies | The Today (default) and Next meeting views, or an empty state with **Sync now** |
89+
90+
Once the App is populated it always shows Ready, never a setup, loading, or
91+
error state again. Populated means any of these, each of which only grows:
92+
93+
- a meeting has Prep status Ready or Failed, or **Prep updated** set;
94+
- a sync succeeded and there are no outside meetings at all, which shows an
95+
empty state explaining that meetings sync hourly and coworker-only
96+
meetings are hidden; or
97+
- two of the 10 most recent syncs have succeeded.
98+
99+
The block also latches populated for the session. Queries report loaded
100+
only after their first real result, and keep their last rows while a
101+
changed query (such as the hourly date cutoff) reloads, so nothing flashes.
102+
After that, a query error is a small notice above the last good data.
103+
104+
In "k of n", taken from the upcoming-meetings query, n counts the meetings
105+
research will cover: not cancelled, with outside attendees, and not yet
106+
ended or being researched now (prep skips meetings that have ended). k
107+
counts those whose Prep status is Ready or Failed, so a failure does not
108+
stall the count. A meeting Queued or Researching with no change (Prep
109+
updated or last edit) for 20 minutes counts as stuck: it leaves n, the
110+
status line suggests Regenerate prep, and the button comes back. A manual
111+
**Research companies and people** run also retries it.
112+
113+
The button adds a Workflow runs row (Trigger "Run now", Started now), which
114+
starts **Sync calendar** like a row added by hand. It is disabled while a run
115+
is waiting, syncing, or researching, and after a click until a new run row
116+
appears or a minute passes. A run that has waited or stayed Pending for more
117+
than 20 minutes (timed from Started, or the row's creation when Started is
118+
empty) is treated as stuck, and the button comes back.
119+
120+
The block reads four live queries (`blocks/main_ui/derive.ts`): Meetings
121+
from local midnight 3 days ago onward (up to 200, not cancelled), the 10
122+
newest Workflow runs, People, and Companies. The next meeting, Today,
123+
progress, and populated signals are all derived from those rows. The SDK
124+
re-sends every subscription's query whenever the host reports a change to
125+
its data source, with no debounce, so research writes multiply requests per
126+
subscription; keep the count low. The Meetings window changes once a day,
127+
so the query is not replaced between renders. When a query fails before any
128+
rows have loaded, the block shows the error with **Retry**, not an empty
129+
state.
130+
131+
Every empty state in the ready UI (no outside meetings yet, nothing today,
132+
nothing upcoming) has a quiet **Sync now** button. It creates the same
133+
Workflow runs row with the same double-click guard. While a sync is in
134+
flight it is disabled and shows the same stage as the setup panel (waiting
135+
for the flow, reading calendar events, then researching k of n meetings),
136+
then clears and re-enables when done, or re-enables with a short note if a
137+
stage looks stuck. The block stays on the ready UI throughout.
37138

38139
"External" means an attendee's email domain is not your account's domain or
39140
one of its coworker domains. Consumer mailboxes such as gmail.com get a People
@@ -54,43 +155,47 @@ npm test
54155
npm run build
55156
```
56157

158+
The custom block's key is `main_ui`: it comes from the file name
159+
`src/customBlocks/main_ui.ts`, and its browser source is in `blocks/main_ui/`.
160+
Its display name is **Perfect Meetings** and its slash command is
161+
`/perfect-meetings`. It opens on **Today**; a viewer's last tab is remembered
162+
in their browser, so new viewers start on Today.
163+
57164
The App's name is the title of its home page. Set it on the first deploy:
58165

59166
```shell
60-
ntn apps deploy --name "Perfect Meetings App"
167+
ntn apps deploy --name "Perfect Meetings"
61168
```
62169

63170
## Run on demand and debug
64171

65-
Every workflow has a manual trigger. Run it with `ntn workers exec` and read
66-
the result with `ntn workers runs list` and `ntn workers runs logs <run-id>`.
67-
You can also run the manual trigger from the workflow in Notion. For a calendar
68-
catch-up, add a row to **Workflow runs**, or run **Calendar ingest** with mode
69-
**backfill**, leaving the other inputs empty. To refresh today's briefs, run **Morning prep** with an empty
70-
date. Wait for the catch-up to finish before refreshing briefs for new meetings.
172+
Every workflow has a manual trigger, and none takes input. Run it from the
173+
workflow in Notion, or with `ntn workers exec`, and read the result with
174+
`ntn workers runs list` and `ntn workers runs logs <run-id>`. For a calendar
175+
catch-up you can also press **Sync calendar** in the block or add a row to
176+
**Workflow runs**. Wait for the catch-up to finish before refreshing briefs
177+
for new meetings.
71178

72179
```shell
73-
# Ingest only your current or next meeting. includeInternal counts coworkers
74-
# as outside attendees for this run only, so you can test with internal meetings.
75-
ntn workers exec calendarIngest -d '{"mode":"next","eventStartTime":null,"eventId":null,"includeInternal":true}'
76-
77-
# Other ingest modes: "backfill" (the hourly scan), "event" (needs
78-
# eventStartTime), and "cancel" (needs eventId).
79-
ntn workers exec calendarIngest -d '{"mode":"backfill","eventStartTime":null,"eventId":null,"includeInternal":null}'
180+
# Calendar catch-up: the hourly scan, on demand.
181+
ntn workers exec calendarIngest -d '{}'
80182

81-
# Rewrite one meeting's brief. reason: "created", "updated", or null (force).
82-
ntn workers exec meetingPrep -d '{"meeting":"<Meetings page URL or ID>","reason":null}'
183+
# Research everyone not yet researched, up to 10 meetings per run.
184+
ntn workers exec meetingPrep -d '{}'
83185

84-
# Run the morning refresh for today, or for another date (YYYY-MM-DD).
85-
ntn workers exec morningPrep -d '{"date":null}'
186+
# Refresh today's briefs.
187+
ntn workers exec morningPrep -d '{}'
86188
```
87189

190+
To rewrite one meeting's brief, tick **Regenerate prep** on it.
191+
88192
The "List calendar events" step in each ingest run records a diagnostic
89193
summary: events per calendar, why events were skipped, and a tally of attendee
90194
domains. When a run finds no meetings, check it first.
91195

92-
To treat coworkers as outside attendees on every trigger, not just one manual
93-
run, set `INCLUDE_INTERNAL_ATTENDEES` on your own worker:
196+
To treat coworkers as outside attendees, for example to test with internal
197+
meetings, set `INCLUDE_INTERNAL_ATTENDEES` on your own worker. It applies to
198+
every trigger:
94199

95200
```shell
96201
ntn workers env set INCLUDE_INTERNAL_ATTENDEES=true

0 commit comments

Comments
 (0)