Skip to content

Design a richer Slack rendering engine with render intents, plugin templates, and Work Object support #208

@dcramer

Description

@dcramer

Junior’s Slack output path is still mostly a slack-mrkdwn pipeline with an optional footer block. That was a reasonable baseline for correctness and delivery stability, but Slack’s rendering surface has moved well beyond that.

As of April 16, 2026, Slack officially supports new Block Kit blocks including alert, card, and carousel, alongside newer agent-oriented surfaces like plan and task_card. Work Objects are also GA and provide a separate path for durable, actionable entity previews inside Slack.

This issue proposes a concrete design plan for taking Junior from “good plaintext in Slack” to “strong native Slack presentation,” without letting the model emit raw Block Kit JSON or weakening the current reply-delivery contract.

Problem

Today, Junior’s final visible Slack replies are effectively constrained to:

  • finalized thread posts
  • top-level text fallback
  • optional section + context block usage for footer metadata

That leaves a lot of product value on the table:

  • the agent cannot deliberately choose richer layouts for different result shapes
  • plugins cannot define reusable domain-specific Slack presentations
  • durable entities like incidents/issues/tickets have no first-class Work Object path
  • the current prompt actively biases the model away from richer layouts like tables
  • the local Slack boundary does not yet reflect the capabilities already available in Slack itself

The result is that Junior is reliable in Slack, but not especially native to Slack.

Current state

Relevant local constraints:

  • final visible replies are standardized around finalized thread posts, not visible streamed prose
  • packages/junior/src/chat/slack/footer.ts narrows SlackMessageBlock to section | context
  • packages/junior/src/chat/slack/reply.ts only attaches blocks for the final footer-bearing chunk
  • packages/junior/src/chat/prompt.ts still instructs the model to emit slack-mrkdwn and avoid tables unless explicitly requested
  • the plugin model is capability/credential/skill oriented today, not presentation oriented

Relevant local opportunity:

  • the installed chat / @chat-adapter/slack stack already supports useful rich surfaces today, including:
    • cross-platform Card(...)
    • native Slack table
    • native Slack plan and task_card
    • structured progress chunks like task_update and plan_update
    • Slack-only stopBlocks

This means our first improvement should not be “invent a new rendering DSL.” It should be “stop collapsing everything back to plain text plus a footer.”

Goals

  • add a concrete, code-owned rendering architecture for rich Slack replies
  • let the agent choose high-level render intents, not raw Block Kit JSON
  • support plugin-owned templates for domain objects like Sentry issues, Linear tickets, and GitHub PRs
  • preserve top-level fallback text and the current finalized-reply contract
  • use Work Objects for durable entities instead of forcing everything through message blocks
  • roll this out in phases, starting with surfaces our installed stack already supports

Non-goals

  • do not reintroduce visible freeform text streaming as the default reply path
  • do not let the model author arbitrary Slack block payloads directly
  • do not make plugin manifests carry raw presentation JSON as static metadata
  • do not treat logs, status telemetry, or tracing output as rendering contracts
  • do not try to solve all Slack surfaces at once before we have a render-intent layer

Design principles

  • keep the rendering contract code-owned and reviewable
  • keep the model at the level of choosing presentation intent, not composing transport payloads
  • preserve accessibility and notification fallbacks with top-level text
  • treat final replies, in-flight progress, and durable entities as different product surfaces
  • make plugin-owned rendering possible without turning plugin manifests into UI configuration blobs
  • use Slack-native richness only behind the Slack boundary
  • degrade gracefully when block support, SDK support, or payload limits get in the way

Proposal

1. Introduce a Slack render-intent layer

Add an internal render contract between assistant output and Slack delivery.

Representative intent types:

  • plain_reply
  • alert
  • summary_card
  • comparison_table
  • result_carousel
  • progress_plan
  • work_object_reference

The model should decide among these intents at a high level. Core code should build the actual Slack view model and fallback text.

This is the main boundary change: the model stops “writing Slack,” and instead starts “choosing how the answer should be presented.”

2. Separate three rendering lanes

Treat these as distinct product surfaces with different policies:

  • final reply surfaces:
    • plain_reply
    • alert
    • summary_card
    • comparison_table
    • result_carousel
  • in-flight progress:
    • assistant status
    • optional plan
    • optional task_card
  • durable entities:
    • Work Objects for records users will revisit, search, expand, and act on

This prevents us from mixing incompatible goals into one abstraction.

3. Add a plugin-owned renderer registry

Extend the plugin system with a code-owned registry for presentation builders.

Each renderer should provide something like:

  • match(result, context): score
  • buildIntent(entity, context): SlackRenderIntent
  • buildFallbackText(entity): string
  • buildActions(entity, context): Action[]
  • buildWorkObject?(entity, context): WorkObjectPayload

This gives plugins ownership over domain presentation while keeping the Slack runtime in control of final rendering, fallbacks, and safety rules.

4. Prefer existing SDK support first

Phase 1 should use capabilities that already exist in our installed stack before we add new Slack-specific block abstractions in Junior.

That means using:

  • Card(...) for better single-object summaries
  • native table when compact comparison is the right surface
  • native plan / task_card for structured progress
  • final-message actions where follow-up workflows matter

This gives us a meaningful first step without waiting for new local type support for every newer Slack block.

5. Add Slack-native block support behind a Slack-only renderer

For blocks that are newer than our current local abstractions, add a Slack-specific renderer behind the outbound boundary.

Initial targets:

  • alert
  • card
  • carousel

This renderer must always emit top-level fallback text and must degrade cleanly to existing card/text layouts when support is unavailable.

6. Use Work Objects for durable records

Use Work Objects for records where persistent identity matters more than one-off message formatting.

Likely first candidates:

  • Sentry incident
  • Sentry issue
  • Linear task
  • GitHub issue or PR

Work Objects should be plugin-owned and should include actions where appropriate.

Layout policy

Use one hero surface per message.

Recommended selection rules:

  • alert
    • warnings
    • blockers
    • partial failures
    • auth required
    • destructive decisions
  • summary_card
    • one important object
    • a few key fields
    • 1-3 next actions
  • comparison_table
    • compact numeric or categorical comparisons
    • dense but still scannable data
  • result_carousel
    • 2-5 comparable objects the user should browse
    • good when the user needs to pick or inspect one item from several
  • progress_plan
    • multi-step, long-running work
    • investigation progress
    • structured tool execution
  • work_object_reference
    • durable record that should expand into richer detail and remain actionable in Slack

For chart-like content, continue to treat Slack as image + concise textual takeaway + accessible download path until Slack ships an official chart surface.

Native agent behavior

We should explicitly teach the agent when rich presentation is the right answer.

Examples:

  • use alert instead of prose when the main point is urgency, risk, or a required human decision
  • use summary_card when the answer is best understood as one record with status, owner, priority, and actions
  • use result_carousel when the user asked for “top few” objects and should be able to browse them
  • use comparison_table when the answer is driven by small structured comparisons
  • use progress_plan for long-running investigations or multi-step tool work
  • use work_object_reference when the object is durable and should exist in Slack as a first-class entity, not just as a formatted message

This should eventually replace the current narrow “emit slack-mrkdwn” prompting bias.

Plugin-defined templates

Plugins should define reusable templates for domain objects, not raw block JSON.

Examples:

Sentry

  • issue summary card
  • incident summary card
  • regression alert
  • issue search-results carousel
  • incident Work Object
  • issue Work Object

Potential card fields:

  • title
  • level or severity
  • status
  • assignee
  • affected users
  • last seen
  • release
  • service or project

Potential actions:

  • open
  • assign to me
  • resolve
  • summarize
  • escalate

Linear

  • issue summary card
  • project or initiative carousel
  • task Work Object

Potential card fields:

  • state
  • priority
  • assignee
  • cycle
  • project
  • due date

Potential actions:

  • open
  • assign to me
  • move to in progress
  • add comment

GitHub

  • PR summary card
  • issue summary card
  • review queue carousel
  • issue or PR Work Object

Potential card fields:

  • repo
  • state
  • author
  • assignee
  • checks
  • review status
  • labels

Potential actions:

  • open
  • assign
  • review
  • summarize
  • close

Work Objects strategy

Work Objects should not be treated as “fancier cards.” They are a separate surface with different product value.

Use them when we want:

  • richer previews from shared links
  • persistent object identity in Slack
  • expandable detail in a flexpane
  • cross-conversation context
  • searchable structured metadata
  • in-context actions on durable records

Recommended first Work Object target:

  • Sentry incident

Reasoning:

  • incident is a strong fit for Slack’s Work Object model
  • incident state is durable and collaborative
  • the value of flexpane detail is obvious
  • actions are meaningful and time-sensitive

Recommended second target:

  • Sentry issue or Linear task

Rollout plan

Phase 1: Foundation

  • introduce SlackRenderIntent
  • introduce a richer internal reply model for Slack
  • decouple final-reply blocks from the current footer-only shape
  • preserve the current finalized-reply contract

Deliverable:

  • the final reply path can carry rich presentation intents without changing visible delivery semantics

Phase 2: Existing-capability uplift

  • enable richer final replies via Card(...)
  • enable native table for compact structured comparisons
  • enable native plan / task_card where useful for progress and possibly finalized summaries
  • add support for final-message actions where they improve follow-up workflows

Deliverable:

  • Junior can produce at least one strong rich final reply surface using capabilities already present in the installed stack

Phase 3: Slack-native richness

  • add explicit support for alert, card, and carousel in the Slack renderer
  • keep graceful fallbacks for unsupported cases
  • ensure fallback text remains first-class

Deliverable:

  • Junior can deliberately choose richer Slack-native surfaces without degrading accessibility or notification behavior

Phase 4: Plugin templates

  • ship first-party templates for Sentry, Linear, and GitHub
  • add scoring and selection rules so the system can choose template vs plain text
  • keep template ownership inside plugin code

Deliverable:

  • at least one domain template is used automatically when it is clearly the best presentation for the result shape

Phase 5: Work Objects

  • implement plugin-owned Work Object mappings for the highest-value durable entities
  • start with one incident-type object and one issue/task-type object
  • add action handling and detail presentation rules

Deliverable:

  • at least one durable plugin entity is represented as a Work Object rather than only as a formatted message

Phase 6: Prompting and evals

  • replace the current “emit slack-mrkdwn” bias with “choose the best render intent”
  • remove the blanket anti-table guidance
  • add evals for:
    • layout choice
    • fallback behavior
    • block-limit handling
    • accessibility
    • template selection
    • action correctness
    • graceful degradation

Deliverable:

  • layout choice becomes deliberate and testable rather than incidental

Acceptance criteria

  • Junior can choose among code-owned render intents instead of only plain Slack markdown
  • final replies can render at least one richer surface beyond footer context
  • plugin templates can own domain-specific rendering without emitting raw Block Kit JSON
  • Slack-only richness stays behind the Slack boundary and does not leak into generic reply contracts
  • top-level fallback text remains present for accessibility and notifications
  • at least one domain template ships for Sentry
  • at least one domain template ships for Linear or GitHub
  • at least one Work Object-backed entity exists
  • prompting and evals are updated so layout choice is deliberate and testable

Risks

  • SDK/type lag:
    • Slack docs may expose blocks before our installed stack types do
  • product regression risk:
    • richer layouts can accidentally weaken the current finalized-reply delivery guarantees
  • accessibility risk:
    • blocks without good top-level fallback text will degrade notifications and screen-reader experience
  • overuse risk:
    • if everything becomes a card/carousel, Slack output becomes noisy rather than better
  • plugin sprawl risk:
    • presentation ownership can become fragmented if the contract is too loose
  • fallback complexity:
    • block limits, unsupported surfaces, and partial support can create brittle UX if not handled centrally

Open questions

  • should plan / task_card remain progress-only, or also be available for finalized investigation summaries?
  • should plugin renderers live in plugin code only, or should core provide shared primitives for common record patterns?
  • what is the minimum viable action model for cards and Work Objects without creating unsafe side effects?
  • which entity should be the first Work Object: Sentry incident, Sentry issue, or Linear task?
  • how much presentation choice should be model-directed versus policy-directed?
  • do we want one cross-platform render-intent layer with Slack-specific lowering, or a Slack-first layer that later generalizes?

References

External Slack docs:

Examples in the wild:

Relevant local code/spec references:

  • packages/junior/src/chat/slack/footer.ts
  • packages/junior/src/chat/slack/reply.ts
  • packages/junior/src/chat/slack/outbound.ts
  • packages/junior/src/chat/slack/output.ts
  • packages/junior/src/chat/prompt.ts
  • specs/slack-agent-delivery-spec.md
  • specs/slack-outbound-contract-spec.md
  • specs/plugin-spec.md
  • packages/junior/src/chat/plugins/types.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions