Watch runs and handle approvals
Stream progress, recover the same run and distinguish an answer from its delivery.
Keep the returned run ID. A network interruption changes what your app can see; it does not authorize starting the action again.
Overview and Runs
The Bot's Overview shows published, live requests from retained history. Tests and sample runs are excluded. Choose 24 hours, seven days or 30 days; daily buckets use the Bot's configured timezone, or UTC when none is set. Each count opens the corresponding Runs filter with the same period. Current queue and waiting counts are separate from that historical period.
Completed means the run completed; it does not mean a customer problem was resolved. Completion time includes queue time and human waits. Reply delivery has its own state: a completed action can still have a reply waiting for delivery. Runs can be filtered by source, lifecycle, files, typed error and delivery state.
Pricing and dollar limits are not configured yet. An unavailable spend card does not mean usage is free or a dollar limit is enforced. Automatic top-up is Coming soon.
Long messages and queued turns
Bot messages accept up to 256,000 characters and 1 MiB of UTF-8 text, subject to any smaller limit imposed by the client sending them. Stacklink preserves the received text and uses the normal context budgeting and conversation retrieval. A long message does not enable Files or become an attachment. Uploaded documents retain the Bot's Files settings and their separate limits.
Slack and Teams can retain up to ten ordered follow-ups behind a conversation's active
turn. Their acknowledgement shows the queued state and an eligible sender can cancel
their queued message. An unresolved approval or connection keeps later turns waiting.
After a failed or stopped turn, later turns are held for explicit recovery. SDK/API
callers retain the existing bot_conversation_busy behavior.
The shared worker gives projects a fair share of available interactive slots. It does
not reserve a fixed number of slots for every Bot. New admissions are bounded per
project; an overloaded project receives HTTP 429 and Retry-After. Reuse the same run
ID when recovering a submission, and do not resubmit an already accepted run.
Stream text and progress
These snippets continue the SDK example. Use them in place of wait() to show output while the existing run is executing.
let after = "0";
for await (const event of stacklink.bots.stream(botId, run.id, { after })) {
after = String(event.sequence); // Persist alongside the run ID for reconnect.
const payload = event.payload as { text?: string };
if (event.type === "model.text.delta" && payload.text) {
process.stdout.write(payload.text);
}
if (event.type === "run.waiting_for_approval" ||
event.type === "run.waiting_for_connection") {
console.log("\nWaiting:", event.payload);
break; // End observation; the saved run remains pending.
}
}
const current = await stacklink.bots.getRun(botId, run.id);
console.log("\n", current.status, current.output ?? current.finalText);from contextlib import closing
after = "0"
with closing(stacklink.bots.stream(bot_id, run["id"], after=after)) as events:
for event in events:
after = event["sequence"] # Persist alongside the run ID.
if event["type"] == "model.text.delta":
print(event["payload"].get("text", ""), end="", flush=True)
if event["type"] in ("run.waiting_for_approval", "run.waiting_for_connection"):
print("\nWaiting:", event["payload"])
break # End observation; the saved run remains pending.
current = stacklink.bots.get_run(bot_id, run["id"])
print("\n", current["status"],
current["output"] if current["output"] is not None else current["finalText"])Model text events can include intermediate text across tool steps. Use the final stored finalText or output as the completed answer.
To reconnect, call stream again with the same run ID and the last saved after sequence. Observation timeouts such as sdk_stream_timeout or sdk_run_timeout do not cancel or restart the run.
When a person needs to act
| Run status | What to do |
|---|---|
queued, running | Continue observing |
waiting_for_approval | Show the approval wait; an eligible person decides the stored request |
waiting_for_connection | Show the required account-connection step |
completed | Use output for JSON or finalText for text |
failed | Show the saved error; do not silently retry a possible external action |
stopped, cancelled | Show that execution has stopped |
waitingOperation and the saved events identify the pending operation. After the person acts, observe the same run.
Connect an account
Creating a connection request requires connections:manage on the application's
server key, in addition to the Bot's run and tool permissions. A restricted key without
that scope remains restricted; enabling an app in the Bot does not grant it to the key.
Keep the key on your server and show the authorized connection URL to the customer.
In SDK 0.3.13 or later, a connection wait contains a server-created action. Render it as a button in your authenticated application; never build an OAuth URL from model text.
const current = await stacklink.bots.getRun(botId, run.id);
const waiting = current.connection ?? current.waitingOperation;
if (waiting?.kind === "connection" && waiting.action?.kind === "open_url") {
// Pass to your own UI. Do not expose the project API key.
const label = waiting.intent === "reconnect" ? "Reauthorize" : "Authorize";
const provider = waiting.providerName ?? waiting.providerKey;
const button = { label: `${label} ${provider}`, href: waiting.action.url };
console.log(button, { expiresAt: waiting.expiresAt, owner: waiting.owner });
}
// After authorization, observe run.id again. Do not submit the task a second time.current = stacklink.bots.get_run(bot_id, run["id"])
waiting = current.get("connection") or current.get("waitingOperation")
if waiting and waiting["kind"] == "connection":
action = waiting.get("action")
if action and action["kind"] == "open_url":
label = "Reauthorize" if waiting.get("intent") == "reconnect" else "Authorize"
provider = waiting.get("providerName") or waiting["providerKey"]
button = {"label": f"{label} {provider}", "href": action["url"]}
print(button, waiting["expiresAt"], waiting["owner"])
# Observe the original run after authorization; do not create another run.Opening the link does not mean authorization succeeded. An expired or completed request has no active connection action. A headless application can store the wait and notify its user through its own interface.
If the link expires, the run ends with connection_request_expired. Its task and
history remain saved. An explicit new message in the same conversation can request a
new link. Do not reuse the expired URL or automatically resubmit completed actions.
The connection field keeps the last connection state even after the wait ends.
To return the customer to your app after authorization, set the return URL when you start the run. Its origin must already be allowed by the project's connection branding:
const run = await stacklink.bots.run(botId, {
sessionId: session.id,
message: "Read my connected account profile.",
connectionReturnUrl: "https://your-app.example/account/connected",
});run = stacklink.bots.run(
bot_id,
session_id=session.id,
message="Read my connected account profile.",
connection_return_url="https://your-app.example/account/connected",
)The URL is validated by the server. It does not grant account access, complete connection, or change the Bot's accepted capabilities.
Show context progress
Compaction happens between model calls, when the next complete request needs a smaller working context. It can happen before a turn, during a tool loop, or after a connection or approval wait. Idle maintenance may also prepare context after the answer completes.
Render these existing stream events as a progress item:
| Event | Suggested display |
|---|---|
conversation.compaction.started | Preparing conversation context |
conversation.compaction.progress | Still preparing context |
conversation.compaction.completed | Context prepared |
conversation.compaction.discarded | Newer conversation context preserved |
conversation.compaction.failed | Context preparation failed; inspect the run's error |
Keep the run's lifecycle authoritative: idle maintenance does not turn a completed answer back into a running answer. Reconnect with the saved event sequence. There is no fixed one-minute completion promise.
Compaction keeps a smaller working summary, not a replacement for execution receipts. Original messages and results remain subject to conversation retention. Completed tool actions are not rerun merely because their transcript was compacted.
A summary can omit details. The Bot can use read_conversation to retrieve retained
messages or a specific tool result when it needs exact earlier information. This works
with Memory Off and stays within the current customer's conversation. If retention policy
no longer permits a detail to be read, the Bot must report it as unavailable. It does
not repeat a completed action to recreate that detail.
Approval and execution are separate: an approved action can still fail. Customer confirmation also does not replace a required company approval. A backend API key cannot impersonate a required human approver.
For eligible customer decisions and the shared request API, see TypeScript approvals and Python approvals.
Stop a run
const stopped = await stacklink.bots.cancel(botId, run.id);
console.log(stopped.status);stopped = stacklink.bots.cancel(bot_id, run["id"])
print(stopped["status"])Stopping observation is different from requesting Stop. Stop cannot undo an external action that already completed.
Recover a callback
The answer and callback have separate outcomes. If the answer completed but your endpoint was unavailable, inspect deliveries:
const { deliveries } = await stacklink.bots.deliveries(botId, run.id);
console.table(deliveries);deliveries = stacklink.bots.deliveries(bot_id, run["id"])
print(deliveries)After fixing the receiver, retry the exact failed delivery selected from that response:
await stacklink.bots.retryDelivery(botId, run.id, deliveryId);stacklink.bots.retry_delivery(bot_id, run["id"], delivery_id)This retries delivery only; it does not regenerate the answer. Verify callbacks with the saved Bot endpoint's signing secret. See Webhook verification.
Avoid duplicate submissions
When starting a run, you can supply your own UUID as runId (TypeScript) or run_id (Python). Persist it before submitting. If admission has an uncertain network result, retry the exact same request with that ID and the same session. A different message or input requires a new ID.
The SDK generates an ID when you omit one. Once you have the returned run ID, recover through getRun / get_run, wait or stream.
Files in a run
SDK 0.3.14 exposes attachments, outputs, citations
and measured coverage on the existing run. Upload finalization means the original
bytes were validated; uploaded does not mean model preparation is complete.
Use attachment.preparing, attachment.ready, attachment.failed and
attachment.read events to update file tiles. Their fileId identifies the same
attachment across reconnects. Read the original run after a stream interruption;
do not upload or submit the task again.
Source links use a reference ID and typed location, such as a PDF page or audio
time. Request a fresh link with bots.files.link when opening the original. A file
made in Workbench uses the same authorized link operation. Signed links expire;
retain file IDs, not their URLs.
A preview reports what was actually read. Copying a full spreadsheet into Workbench does not by itself establish that every row was analyzed. SQL and code uploads are read as data; they do not authorize execution.
Slack and Teams runs in staging
Runs show the source app, requesting person, channel/thread and reply delivery separately from execution. A successful action can still have a failed reply delivery. Retry a known failed delivery without running the action again; an uncertain send is shown as unknown and is not automatically duplicated.
Connection and approval waits preserve the original run. The Bot replies to the original destination only while its deployment, sender and destination remain authorized. Held follow-ups offer Send and Cancel in Runs.
AI usage and spending limits
When AI billing is enabled, the workspace allowance and any Admin-authorized additional spending cover the supported Bot controller models. A Bot may also have its own monthly AI cap. The Bot cap restricts spending; it does not grant additional workspace spending. Auto top-up is Coming soon.
Each model call reserves its maximum permitted cost before dispatch, then settles actual provider-reported usage. Current workspace Admins can see costs and change limits in the dashboard. SDK/MCP callers do not receive private workspace amounts. Historical unpriced runs stay unpriced; they are not billed retroactively.
A limit error means the next model call did not start. Retain the original run when a response or stream is lost. An unknown provider outcome keeps its usage reservation for reconciliation; submitting the task again can create another call and charge.
Audio transcription uses Scribe v2 at a provider rate of $0.22 per recorded hour plus 50%, giving $0.33 per hour before applicable taxes. Thirty minutes is $0.165; the Bot's subsequent reasoning and tools are separate usage. The operation reserves against the existing workspace allowance and Bot budget before submission, then settles once from the provider's duration receipt. Historical transcripts are not charged retroactively. Follow-up reads do not repeat the transcription charge.
During preparation a run uses waiting_for_execution with
waitingOperation.kind: "attachment_preparation". Keep observing that same run;
it resumes automatically. Duplicate completion notifications do not create another run
or charge. Stop prevents further Bot execution, while a transcription already submitted
may still incur its recorded processing cost. A lost submission response remains under
reconciliation instead of starting another paid request automatically.
bot_helper_pricing_unavailable means the requested helper did not start. Audio is
available only after the deployment's provider account and billing verification are complete.
Original recordings remain retained and previously prepared transcripts remain readable.
Web-search availability depends on its supported model-specific rate. Other file reading
and configured tools keep their existing rules.