The design for the standup and EOD check-ins (Phase 2 onward). Decided 2026-10-12.
Principles
- Everything stays in context.
- Every button, menu, card and reply carries the check-in it belongs to (
person:date:kind) and the ClickUp task it is about. - An answer changes that task, is commented on that task when submitted, and is summarised in the person's comment on that day's Daily Standup.
- Every button, menu, card and reply carries the check-in it belongs to (
- Changes reach ClickUp as they are made; words wait for submit. (Changed 2026-10-15: an unsubmitted standup
used to lose its time and decisions.)
- In a check-in, each answer's changes to tasks and time are written by the queue straight after the click
(
checkin.sync,sync.ts), as the person. Sally keeps what it wrote per slot (thesyncedtable: a task's fields, a time entry, a time entry's note), writes only the difference on the next change, and puts a field back when its answer is taken back, but only while ClickUp still shows what Sally wrote. - Comments on tasks, blocker notices and the Daily Standup comment are posted on Submit (or at the cutoff), so a change of mind never leaves comments and notifications behind.
- Outside a check-in, a form's own Save button is the confirmation: the Work Object side panel, and the Edit and Deployed forms opened from a command, a shortcut or App Home. Those are applied immediately, as the person.
- Nothing outside a form writes on a click alone. App Home's rows open the form rather than staging anything.
- In a check-in, each answer's changes to tasks and time are written by the queue straight after the click
(
- Deterministic rendering. Models (Jev) only make typed judgments, such as "which task is this note about?". They never write Slack markup.
Layout
One root message per person per day is that day's standup. Everything else is a reply in its thread, so each day is self-contained and the 50-block message limit never binds.
| Message | Built from | What it does |
|---|---|---|
| Root | header, section, actions | A summary of the look-back day and today; a progress checklist ("3 decisions left · Today reviewed · Blockers: none"); Add a blocker / No blockers / Question or concern; Review (a link into the thread, checkins.thread_url), Submit (opens the confirmation), Snooze, I'm out today. |
| Needs a decision | A numbered list (section + actions per task), or a carousel of card blocks (rule decisionLayout) | One row per question: missing time, vague notes, planned-but-untouched work, and the most recently overdue tasks. Buttons: the likely answer, plus More…. The list is the default: the pilot found the carousel harder to scan (2026-10-13). |
| Needs you now | Rows grouped by status (section + actions per task) | Every task in a spotlightStatuses status, whatever its dates, outside Today's cap. Each row leads with what its status needs: Deployed… (a deploy) or an estimate select (an estimate), then Edit and the task menu. At most 18 rows, to stay inside 50 blocks. |
| Today | Work Objects, or Ticket-List rows | One entry per task, ordered by due date then priority, each with five actions: Done, New date, Blocked, Reassign, Add a note. Above them, whether the day fits and how long carried-over work has slipped. Below them, "To fit the day" moves when over capacity, and menus for a missing estimate or priority. |
| Older than a year | button → modal, one linked row per task | A clean-up for ancient overdue tasks. Each row shows where the task lives, its creator and its last change, with a menu: new date, done, won't do, give back, hand to someone, or not sure (a PM decides). |
Edit and Deployed open one form (blocks/edit.ts): status, due date, priority, estimate, time with the day it was
spent, and a note, prefilled from the task and anything already answered. Deployed is the same form cut down to status
(required), time and note. The status select comes from the task's list, with the nextStatuses rule's likely next
statuses in their own group first. Lists' statuses are cached in D1 (list_statuses, refreshed weekly and warmed when
a draft is built), so the form opens inside 3 seconds; a list not yet cached opens without the select and the queue
fills it in with views.update, which keeps what was typed because block and action ids match. A saved form replaces
any one-click answer it overlaps (a status over Done, a due date over New date).
More… and the row actions open modals. They can push a second step without losing the first (Slack allows three views):
- a date picker for New date,
- a person picker for Reassign, defaulting to the task's creator,
- the blocker form for Blocked,
- a priority select for Bump priority.
Replies in the thread are notes. Whatever the person types in the day's thread is matched to one of their tasks by Jev (a typed choice over the day's tasks). Sally confirms with buttons: "Add this to NEIT-1498? Yes / Another task / Just the standup". A note never attaches to a task silently.
Review opens a modal:
- an
alertblock for anything unanswered, - the full list of writes that will happen, as the person.
On confirm, the root message shows a checklist that ticks each write off as it lands in ClickUp.
Work Objects
ClickUp tasks are posted as Work Objects (slack#/entities/task) in the Today reply.
| Element | Content |
|---|---|
| The card | Title with display_id (the task key), product "ClickUp". Status (tag colour by status type), due date, priority and assignee. Estimate and time logged as custom fields. |
| Primary actions (2) | Done, New date |
| Overflow actions (5) | Blocked, Reassign, Bump priority, Add a note, Won't do |
The side panel (entity_details_requested → entity.presentDetails) | Built live from ClickUp with the viewer's own token. Status, due date, priority and assignee are editable. Edits arrive as view_submission with view.type: entity_detail. |
Edit handling:
- Sally re-checks that the viewer is a connected person who may act on the task.
external_refandentity_urlare untrusted. - It applies the change as them (dry run respected) and refreshes the panel.
Unknowns, and the fallback:
- The app setting: Work Object Previews, with the Task entity type. In the manifest that is
features.rich_previews, plus theentity_details_requestedevent. No scopes beyondchat:writeare needed. - What Slack doesn't document: how many entities one
chat.postMessagecan carry, and whetherchat.updateacceptsmetadata.entitiesat all. Its docs and both SDKs describe only the event form of metadata there. Sally re-renders the Today message withchat.update, so this is the deciding check. - The rule
taskCardstherefore chooseswork_objectsorrows, defaulting torowsuntil the spike confirms Work Objects on the Linchpin plan. - Both render from the same task data, and both send the same five actions.
Agent mode
Planned, not yet on.
Why it's tested on a separate app first:
- Irreversible. Turning on
features.agent_view(the Agents setting) cannot be undone. - It changes the DM for everyone. Threads move into a timeline above the composer.
- It locks out guests. Workspace guests cannot use an app with Agents enabled.
- Some workspaces don't have it. It can fail with
feature_disabledon a workspace without it.
So a "Sally (dev)" app, created from a copy of the manifest with agent_view added, goes into a Slack developer
sandbox first. Then:
- Sally is declared an agent app, which is irreversible.
- Each standup becomes a titled session ("Standup · Mon Oct 12"):
suspendedwhile waiting on the person,processingwhile writing to ClickUp,closedonce submitted.
The thread-per-day layout above is the same either way.
EOD (Phase 4)
The same root-plus-thread layout, with Today's plan and Done today replies in place of Today and the clean-up. Every task uses the standup's row: the task and its facts in a section, then an actions block. (Until 2026-10-15 the plan was a to-do list of checkboxes. The pilot found it hid the status and the time a finished task needs.)
- Today's plan holds the tasks still to decide. Each row has Finished…, a Not finished… select (Wrapping up tonight, Move to the next working day, Pick a date…, Blocked…), Why? once one is picked, Edit and a menu (Reassign, Add a note). Today's time leads the facts, and no time is flagged.
- Finished… is the Edit form in
finishedmode: a required status (the rule's likely next ones, then the list's done and closed statuses, then the rest), time to log and what got done. It saves aneditanswer withfinished: true, which replaces any not-finished answer; picking Not finished… afterwards takes the status back and keeps the time and note. - Done today holds what ClickUp has finished or handed on (a waiting status: "in client review" is done-type in ClickUp but waits on the client), with Log time… when nothing was logged today, then time on tasks outside the plan. It is always posted, so a task finished later in the day has somewhere to go.
- Each answer is written as it is made; the comments wait for submit.
The old buttons (plan_done checkboxes, plan_tonight and the rest) are still handled, for EODs posted before the
change.
Alerts
Slack's alert block works in modals only. In messages, urgency goes in a callout (a coloured box of sections, rich
text or images, with no actions or context blocks inside; its colours are green, orange, blue, purple, gray and pink,
with no red or yellow): Needs you now on the root and its reply, and who is blocked by whom on the digest, all
orange. In the Edit,
Deployed and Finished forms, alerts lead: error for an overdue task, warning for a running ClickUp timer, a
finish with no time today, due today, or a missing estimate, and info for a deploy waiting. The saved view ends
with a success, info (dry run) or error alert.
Live statuses
A draft is a snapshot, but threads show what ClickUp says now (live-status.ts):
- Drawing overlays the
taskStatusUpdatedrows the webhook stored afterdraft.generatedAt(liveDraft). The EOD plan's finished and waiting are worked out again from them. The digest and Team's "Needs them now" use it too. - The webhook enqueues a redraw of any open check-in (sent or in review, last two days) that names the task.
- Writing a status, Sally reads the task first (
checkStatus). It leaves the status alone if it is already there, if a Done or Finished… meets a task that is finished already, or if it moved after the person chose (edits carrystatusFrom). The skip is logged in the Activity log. On 2026-10-14 a blind Done moved two tasks closed as "complete" back to "resolved".
App Home (Phase 5)
Slack gives apps no tabs inside Home, so a row of buttons along the top re-publishes the view, the one showing
highlighted. The tab rides in the view's private_metadata, and app_home_opened carries it back, so Home reopens on
the tab it was left on.
A tab click is drawn from the interactions route (homeTabClicked in home.ts), not the queue, and Slack's answer
waits for it (up to 1.5 seconds) the same way a message click does. So the button stays busy until the new tab is
showing, rather than going quiet while the click waits its turn. Block Kit has no disabled buttons, so repeat clicks
are handled instead:
- Needs you now shows "Loading…" at once and records when it started. The queue fills it from ClickUp, publishing
with the loading view's
hash. If the person has moved to another tab by then, Slack refuses the hash and the late answer is dropped instead of pulling them back. - Clicking Needs you now again while it loads does nothing, for up to 20 seconds. After that a click starts it again, in case a load never finished.
- Clicking the tab already showing redraws it. On Needs you now that asks ClickUp again.
- If Slack can't be reached from the route, the queue draws the tab instead.
- Your day (D1 only, so it opens instantly):
- Am I set up? ClickUp connection, whether check-ins are on, and dry run.
- today's standup and EOD, each with its state and an Open link, or Start … now,
- the Today list as the standup drafted it, with decisions so far and whether the day fits, and how many tasks need the person now,
- recent check-ins.
- Needs you now: live from ClickUp ("Loading…" first, then the list), each task with Deployed… or Edit. Save writes straight away.
- Team, for PMs and admins. Who has what waiting on them, from the day's standup drafts, then the digest as of now: the standup board until an hour before the EOD digest, then the EOD board.
- Shortcuts: every command and shortcut, from
COMMANDSincore/commands.ts, the same list/sally helpshows.
Commands and shortcuts
See Commands and shortcuts for what people see.
/sally <verb>is parsed byparseCommand. Every verb is a full word;estandpriare the only short forms. Verbs that change one thing (estimate,priority,due,log,done) go to the queue ascommand.runand answer through the command'sresponse_url.editanddeployedopen the form at once with the trigger and a "Loading…" view, and the queue (task.edit_load) fills it in. A task named by key is a custom task id, fetched withcustom_task_ids=true.- Update a ClickUp task (global shortcut) and Add to a ClickUp task (message shortcut) open a picker: an
external_selectwhose options come from/slack/options(the manifest'smessage_menu_options_url). It answers from D1 alone, from the person's latest draft, plus whatever key or link was typed. Picking loads the task into the edit form, with the message (and a link back) as the note for the message shortcut. - The standup message ends with a line saying where the list is.
Limits that shape this
| Element | Limit |
|---|---|
| Message | 50 blocks |
| Modal / Home | 100 blocks |
| Section text | 3,000 characters |
| Overflow | 5 options; text ≤75, value ≤150 |
| Card | ≤3 buttons; title ≤150 characters, body ≤200 |
| Carousel | ≤10 cards |
| Checkboxes | ≤10 options |
| Modal stack | ≤3 views |
trigger_id | Valid 3 s, usable once |
private_metadata | ≤3,000 characters |
| Work Object actions | ≤2 primary + ≤5 overflow |
| Option group | ≤100 options; a select's value ≤150 |
| Options Load URL | Answered within 3 s, so D1 only |