How Sally talks in Slack

Last updatedView as Markdown

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.
  • 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 (the synced table: 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.
  • 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.

MessageBuilt fromWhat it does
Rootheader, section, actionsA 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 decisionA 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 nowRows 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.
TodayWork Objects, or Ticket-List rowsOne 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 yearbutton → modal, one linked row per taskA 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 alert block 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.

ElementContent
The cardTitle 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_ref and entity_url are 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 the entity_details_requested event. No scopes beyond chat:write are needed.
  • What Slack doesn't document: how many entities one chat.postMessage can carry, and whether chat.update accepts metadata.entities at all. Its docs and both SDKs describe only the event form of metadata there. Sally re-renders the Today message with chat.update, so this is the deciding check.
  • The rule taskCards therefore chooses work_objects or rows, defaulting to rows until 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_disabled on 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"): suspended while waiting on the person, processing while writing to ClickUp, closed once 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 finished mode: 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 an edit answer with finished: 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 taskStatusUpdated rows the webhook stored after draft.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 carry statusFrom). 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 COMMANDS in core/commands.ts, the same list /sally help shows.

Commands and shortcuts

See Commands and shortcuts for what people see.

  • /sally <verb> is parsed by parseCommand. Every verb is a full word; est and pri are the only short forms. Verbs that change one thing (estimate, priority, due, log, done) go to the queue as command.run and answer through the command's response_url. edit and deployed open 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 with custom_task_ids=true.
  • Update a ClickUp task (global shortcut) and Add to a ClickUp task (message shortcut) open a picker: an external_select whose options come from /slack/options (the manifest's message_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

ElementLimit
Message50 blocks
Modal / Home100 blocks
Section text3,000 characters
Overflow5 options; text ≤75, value ≤150
Card≤3 buttons; title ≤150 characters, body ≤200
Carousel≤10 cards
Checkboxes≤10 options
Modal stack≤3 views
trigger_idValid 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 URLAnswered within 3 s, so D1 only

Have a question about this page?

Linchpin assistant

Was this helpful?