Run with the SDK
Call a published Bot from TypeScript or Python, then continue the same conversation.
Use this code on your backend. Stacklink hosts the agent, so these examples do not need another agent SDK or your own model loop.
Before you start
- Publish a Bot with Your app enabled. Copy its Bot ID from Use it.
- Create a project API key with
sessions:create,sessions:read,bots:readandbots:run. These scopes cover a basic Bot that answers from your message. - If the Bot calls connected apps, Your API or custom MCP tools, also grant
tools:execute. Bot configuration, account access and approval rules still apply. - The examples keep the session’s default Ask first policy. Explicitly setting
providerMutationModerequiressessions:manage; do not add that permission just to run these examples. - Use the published SDK, 0.3.13 or later. TypeScript requires Node.js 22+; Python requires 3.10+.
Install the current SDK
These examples use 0.3.13, including typed connection actions, timezone configuration
and context progress. Use https://api.stacklink.in for production. Staging uses
https://api-staging.stacklink.in with a separate staging project and API key.
npm install @stacklink/sdkuv add stacklink-sdkSet STACKLINK_API_KEY and STACKLINK_BOT_ID in your backend environment. Keep the key out of browser code.
Keep the execution key restricted
Running a published Bot does not require bots:manage. That scope is for creating, editing and publishing Bots. API keys need explicit Bot permissions; tools:execute alone does not include bots:run. Stacklink rechecks the original key before execution and resumption, so revoking the key or removing a required permission can stop pending work.
Get the first answer
import { Stacklink } from "@stacklink/sdk";
const stacklink = new Stacklink({
apiKey: process.env.STACKLINK_API_KEY!,
baseUrl: "https://api.stacklink.in",
});
const botId = process.env.STACKLINK_BOT_ID!;
// Demo IDs. In your app, derive these from your authenticated user.
const session = await stacklink.sessions.create({
tenantId: "demo-company",
userId: "demo-user",
});
const run = await stacklink.bots.run(botId, {
sessionId: session.id,
message: "Notes: Priya will send the proposal on Friday. Summarize the next step.",
});
console.log({ sessionId: session.id, runId: run.id,
conversationId: run.conversationId });
const result = await stacklink.bots.wait(botId, run.id);
if (result.status === "completed") {
console.log(result.output ?? result.finalText);
} else {
console.log({ status: result.status,
waiting: result.waitingOperation, error: result.error });
}import os
from stacklink import Stacklink
stacklink = Stacklink(
api_key=os.environ["STACKLINK_API_KEY"],
base_url="https://api.stacklink.in",
)
bot_id = os.environ["STACKLINK_BOT_ID"]
# Demo IDs. In your app, derive these from your authenticated user.
session = stacklink.sessions.create_bound({
"externalTenantId": "demo-company",
"externalUserId": "demo-user",
})
run = stacklink.bots.run(
bot_id,
session_id=session.id,
message="Notes: Priya will send the proposal on Friday. Summarize the next step.",
)
print({"sessionId": session.id, "runId": run["id"],
"conversationId": run["conversationId"]})
result = stacklink.bots.wait(bot_id, run["id"])
if result["status"] == "completed":
print(result["output"] if result["output"] is not None else result["finalText"])
else:
print({"status": result["status"],
"waiting": result["waitingOperation"], "error": result["error"]})run() returns a saved run, not necessarily a finished answer. wait() returns when the run finishes or needs an approval/account connection. Handle those states →
Save the session, conversation and run IDs in your application's conversation record. A browser request ending must not discard them.
Published identity in the staging candidate
The native-identity staging candidate also exposes publishedIdentity on bots.get(). It contains the published name, public description and picture URL, or null before publication. Use that field for a customer-facing identity; identity describes the editable draft. This addition has not yet been promoted to production.
const { publishedIdentity } = await stacklink.bots.get(botId);published_identity = stacklink.bots.get(bot_id).get("publishedIdentity")Generated avatars
TypeScript and Python SDK 0.6.2 include generated avatars, draft and published identity, Workflows and embedded authoring. Use the public npm and PyPI packages with the production API.
Generated identity stores a versioned avatar design alongside its server-created PNG:
const bot = await stacklink.bots.create("SalesAssistant", {
publicDescription: "Helps the sales team prepare proposals.",
avatar: { version: 1, style: "clay", seed: "SalesAssistant", tone: "mint" },
});
const picture = bot.identity.pictureUrl;bot = stacklink.bots.create(
"SalesAssistant",
avatar={"version": 1, "style": "clay", "seed": "SalesAssistant", "tone": "mint"},
)
picture = bot["identity"]["pictureUrl"]Styles are clay, pebble, shapes and letters. Tone is auto or one of mint,
teal, sky, iris, plum, rose, coral, apricot, sun, lime, slate, ink.
New Bots without an uploaded icon receive a saved Clay design and PNG. Renaming alone
preserves the seed and picture. To change initials for Letters, explicitly update its seed.
Custom uploads use the existing iconAssetId with avatar: null.
identity.avatar describes the draft; publishedIdentity.avatar remains at the accepted
publication until republished. Use pictureUrl for a static image in your application,
Slack or Teams. Dashboard expressions and motion follow live run state; they do not
change those exported images. No extra tool or provider permission is needed.
Send the next message
After the previous run completes, reuse the same customer session and returned conversation ID:
const next = await stacklink.bots.run(botId, {
sessionId: session.id,
conversationId: run.conversationId,
message: "Make that one sentence.",
});
const answer = await stacklink.bots.wait(botId, next.id);
console.log(answer.status, answer.output ?? answer.finalText);next_run = stacklink.bots.run(
bot_id,
session_id=session.id,
conversation_id=run["conversationId"],
message="Make that one sentence.",
)
answer = stacklink.bots.wait(bot_id, next_run["id"])
print(answer["status"], answer["output"] if answer["output"] is not None else answer["finalText"])Use variables for inputs declared in the Bot, such as an order number. They do not establish customer identity or permission to read that order.
Keep the session while work is pending
An approval can outlive your HTTP request. Do not close its session in a request-level finally block. Keep it available for continuation; close it when the conversation is finished and no run is pending.
// Only after this conversation and its pending work are finished:
await session.close();# Only after this conversation and its pending work are finished:
session.close()Create and publish from code
Prefer the builder for visual configuration. For automation, add bots:manage to your management key and use the returned revision on every save:
let bot = await stacklink.bots.create("Meeting notes");
bot = await stacklink.bots.update(bot.id, bot.revision, {
instructions: {
...bot.instructions,
text: "Summarize the supplied notes into concise next steps. Never invent owners or dates.",
},
});
await stacklink.bots.publish(bot.id, bot.revision, {
app: true, mcp: true, team: false,
});
console.log(bot.id);bot = stacklink.bots.create("Meeting notes")
bot = stacklink.bots.update(bot["id"], bot["revision"], {
"instructions": {
**bot["instructions"],
"text": "Summarize the supplied notes into concise next steps. Never invent owners or dates.",
},
})
stacklink.bots.publish(bot["id"], bot["revision"], {
"app": True, "mcp": True, "team": False,
})
print(bot["id"])Nested configuration objects replace that object; preserve its existing fields when changing one setting. If a revision conflicts, reload and review the newer configuration before saving.
Stream the answer and handle failures →
Send a file
These examples use SDK 0.3.14 or later. Uploading and reading a file uses the basic Bot scopes above; it does not require enabling Workbench. Workbench analysis retains its separate permissions.
Install the attachment-capable package for these examples:
npm install @stacklink/sdk@0.3.14
# Python (sync and async clients):
python -m pip install stacklink-sdk==0.3.14// Existing authenticated customer session; SDK 0.3.14 or later.
const bytes = await (await import("node:fs/promises")).readFile("invoice.pdf");
const upload = await stacklink.bots.files.createUpload(botId, {
sessionId: session.id, name: "invoice.pdf",
mediaType: "application/pdf", sizeBytes: bytes.length,
});
const sent = await fetch(upload.uploadUrl, {
method: "PUT", headers: upload.requiredHeaders, body: bytes,
});
if (!sent.ok) throw new Error("Upload failed");
const { file } = await stacklink.bots.files.finalize(botId, upload.fileId, {
sessionId: session.id,
});
const run = await stacklink.bots.run(botId, {
sessionId: session.id, message: "Explain this invoice and cite its pages.",
attachments: [file.id],
});
for await (const event of stacklink.bots.stream(botId, run.id)) {
if (event.type === "model.text.delta") console.log(event.payload.text);
if (event.type.startsWith("attachment.")) console.log(event.type, event.payload);
if (["run.waiting_for_connection", "run.waiting_for_approval"].includes(event.type)) break;
}
const current = await stacklink.bots.getRun(botId, run.id);
console.log(current.status, current.citations, current.outputs);from pathlib import Path
from urllib.request import Request, urlopen
content = Path("invoice.pdf").read_bytes()
upload = stacklink.bots.files.create_upload(
bot_id, session_id=session.id, name="invoice.pdf",
media_type="application/pdf", size_bytes=len(content),
)
with urlopen(Request(upload["uploadUrl"], data=content,
headers=upload["requiredHeaders"], method="PUT")) as sent:
assert 200 <= sent.status < 300
file = stacklink.bots.files.finalize(
bot_id, upload["fileId"], session_id=session.id,
)["file"]
run = stacklink.bots.run(
bot_id, session_id=session.id,
message="Explain this invoice and cite its pages.", attachments=[file["id"]],
)
for event in stacklink.bots.stream(bot_id, run["id"]):
if event["type"] == "model.text.delta":
print(event["payload"]["text"])
if event["type"] in ("run.waiting_for_connection", "run.waiting_for_approval"):
break
current = stacklink.bots.get_run(bot_id, run["id"])
print(current["status"], current.get("citations"), current.get("outputs"))Async Python uses the same bots.files and bots.run calls with await, and async for
for streaming. The SDK does not force a UI framework.
uploaded means the server validated and froze the bytes. The run then reports preparing,
ready or failed. Read attachments[].error for typed failures such as audio_too_long.
Use bots.files.get to recover file status and bots.files.link for a fresh authorized link.
Continue with the returned conversationId; the Bot can read earlier retained attachments without
uploading them again. After a disconnect, observe the original run ID instead of resubmitting the message.
A retry with the same run ID and different attachments is rejected.
Citations carry an exact source ID, file ID and typed page/time/range locator. In Markdown,
resolve stacklink-source:ID against current.citations; never match words such as “page 2”.
JSON answers retain their configured schema, with citations alongside the answer. Generated outputs
use the same authorized file-link operation. Links expire; file identity does not change.