Skip to main content
Some commands start work that takes a while and has stages a member cares about: a deploy goes from queued to building to live, an import counts rows, a report runs its queries. Instead of answering once, or deferring and answering at the end, your handler can stream its answer. The first answer is posted as usual, and every later one replaces it in place: the chat message, the notice and the block’s props. Stream when the stages are worth watching. For work that only has a result, defer and answer once.

Stream from an async generator

Make the handler of a slash command or message action an async function*. Each yield is an answer, the same shape a handler returns. The generator’s return value, if it has one, is the last update.
src/deployCommand.tsx
The yields are typed like a handler’s answer, so Deploy.insert checks its props and the app must include Deploy. If the generator throws after its first answer, the SDK finishes the answer with the failure notice, “Couldn’t finish this. Try again.”, and logs the error.

Stream from an Effect

On the Effect API, return a Stream of answers. Services and failures in the stream become the command’s requirements and errors. See Progress updates in the Effect guide.

How the answer moves

The SDK starts the generator or stream when the delivery arrives:
  1. If the first answer comes within 3 seconds, it’s the delivery’s answer, the same as a normal handler’s.
  2. If it’s slower, the SDK answers the delivery with 202 and your app’s row reads “is thinking…” until the first answer arrives. Only the member who ran the command sees that row. Pass progress: { visibility: "public" } to the definition to show it to everyone in the circle or direct message.
  3. Every later answer is sent as an update. The SDK waits 500ms after an answer arrives, then sends the latest one, so a generator that yields faster than that skips the stages in between.
  4. When the generator returns or the stream ends within 500ms of its last value, the SDK sends that value once, as the final update. If the stream ends later, that value has already gone out as an update, so the SDK sends it again as the final update to close the answer. The answer can’t change after that.
The SDK keeps running the work after the delivery returns, with waitUntil. Hosted runtimes keep that work alive for a limited time, so hand work that runs for many minutes to a queue and update the answer from there, as described in Update from somewhere else.

Blocks and visibility

The first answer decides the visibility. A later answer with content or a block repeats the same visibility, and so does the generator’s return; the types require it, and the SDK rejects one that leaves it out. A later answer that’s only a notice keeps the first answer’s visibility. Changing it is an error. A later answer’s block must come from the same block definition as the first answer’s block. Its props replace the block’s props and the block renders again. Leave block out to keep the block as it is. Switching to another block, or adding a block when the first answer had none, is an error. When the stream breaks either rule, the SDK stops it, logs ReintersectAppProgressMismatchError (a defect on the Effect API), and finishes the answer with the failure notice.

Limits

There’s no limit on the number of updates. When the 2 hours pass, or 15 minutes go by without an update, Reintersect stops taking text and notice changes. If your stream keeps going, the SDK keeps sending its block changes on their own, so the block stays current while your app reports at least every 15 minutes. These updates need no extra scope, because the member’s command created the block. The answer ends when the stream does.

Errors

Update from somewhere else

When a queue worker or another service does the work, defer the answer, pass invocation.interactionToken along, and send updates with the app client. Answer first with invocations.respond, then call invocations.update, with final: true on the last one:
src/deployWorker.ts
Without the SDK, send POST /v0/invocations/update with the install token and { interactionToken, answer, final? }.

Testing

host.runCommand returns the first answer, or { status: "deferred" } when the first answer took longer than the 3-second budget on the test clock. While your handler waits on the test clock, such as an Effect.sleep, the clock jumps ahead; while it waits on real work, such as a fetch, the clock keeps pace with real time, so only work that really takes 3 seconds is deferred. host.settle() runs the stream to its end and moves the test clock through the SDK’s waits. host.answersOf(interactionToken) returns every answer the member saw, in order, and whether the answer is final. host.expectProgression checks that answers matching each step arrived in that order, with any other answers in between, and that the answer finished. A stage that coalescing skipped never reaches the member, so a step that names it fails, and the failure says which step went missing. Name only the stages that last at least 500ms, such as the first answer and the final one:
Each update is also an invocations.update push with its final flag.