# How to let a Slack bot run code safely Acknowledge Slack within 3 seconds, run the snippet in a microVM with no internet and a time limit, then post the output to response_url. **On Runtime each Slack channel can keep its own machine that pauses when the channel goes quiet.** A sandbox paused after two idle minutes keeps its files, installed packages and memory for $0.08 per GB per 30-day month, and the next `/run` wakes it, usually in about half a second. While it runs, a 1 vCPU, 2 GiB sandbox costs $0.01625 an hour waiting, because Runtime bills measured CPU at $0.025 per vCPU-hour ([pricing](/docs/pricing), 25 September 2026). ## What Slack requires A slash command reaches your app as a form-encoded POST with fields such as `text`, `response_url`, `channel_id`, `user_id` and `team_id`. Slack's documentation says the acknowledgement "must be received by Slack within 3000 milliseconds", and that a `response_url` accepts up to 5 responses within 30 minutes. So the bot answers at once, runs the code after, and posts the result to `response_url`. With `response_type: "in_channel"` the whole channel sees it; the default, `ephemeral`, shows it only to the person who asked. ## The short answer Verify that the request came from Slack before anything runs. Then acknowledge, and do the work after the response has gone: ```ts check import { Sandbox } from "withruntime"; const TICKS = "`".repeat(3); // a Markdown code fence function unfence(text: string) { const t = text.trim(); if (t.length < 6 || !t.startsWith(TICKS) || !t.endsWith(TICKS)) return t; return t.slice(3, -3).replace(/^\w*\n/, ""); // drop a language tag such as "python" } async function runAndReply(form: URLSearchParams) { const code = unfence(form.get("text") ?? ""); const sbx = await Sandbox.getOrCreate(`slack-${form.get("team_id")}-${form.get("channel_id")}`, { vcpu: 1, memoryMiB: 2048, idlePauseSeconds: 120, network: { internet: false }, labels: { app: "slack-runner" }, }); await sbx.files.write("/workspace/snippet.py", code); const run = await sbx.exec(["python3", "/workspace/snippet.py"], { timeoutMs: 20_000 }); const output = (run.stdout + run.stderr).slice(-2_800) || "(no output)"; const status = run.timedOut ? "timed out after 20 s" : `exit ${run.exitCode}`; await fetch(form.get("response_url")!, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ response_type: "in_channel", text: `${status}\n${TICKS}${output}${TICKS}`, }), }); } // Your HTTP route for the slash command, after checking Slack's signature. export async function slashCommand(request: Request): Promise { const form = new URLSearchParams(await request.text()); void runAndReply(form).catch((error) => console.error(error)); return new Response("Running…"); // the acknowledgement, well inside 3 seconds } ``` ```python check import json import re import threading import urllib.request from withruntime import Sandbox TICKS = "`" * 3 # a Markdown code fence def unfence(text: str) -> str: t = text.strip() if len(t) < 6 or not (t.startswith(TICKS) and t.endswith(TICKS)): return t return re.sub(r"^\w*\n", "", t[3:-3]) # drop a language tag such as "python" def run_and_reply(form: dict) -> None: code = unfence(form.get("text", "")) sbx = Sandbox.get_or_create( f"slack-{form['team_id']}-{form['channel_id']}", vcpu=1, memory_mib=2048, idle_pause_seconds=120, network={"internet": False}, labels={"app": "slack-runner"}, ) sbx.files.write("/workspace/snippet.py", code) run = sbx.exec(["python3", "/workspace/snippet.py"], timeout_ms=20_000) output = (run.stdout + run.stderr)[-2_800:] or "(no output)" status = "timed out after 20 s" if run.timed_out else f"exit {run.exit_code}" body = json.dumps({"response_type": "in_channel", "text": f"{status}\n{TICKS}{output}{TICKS}"}).encode() request = urllib.request.Request(form["response_url"], data=body, headers={"content-type": "application/json"}) urllib.request.urlopen(request, timeout=10) def slash_command(form: dict) -> str: """Call from your web framework's route, after checking Slack's signature.""" threading.Thread(target=run_and_reply, args=(form,), daemon=True).start() return "Running…" ``` - The sandbox is found by name, so every `/run` in a channel lands on the same machine, woken if it paused. Variables do not carry over between runs, but files the snippets write do. - The snippet runs as a file with an array command: nothing in it is read by a shell on your side or in the sandbox. - The bot posts the last 2,800 characters of output, so a snippet that prints a megabyte still produces a readable message. A result holds at most 64 KiB of each stream in any case. ## What each setting stops | A snippet that... | What stops it | Setting | | ----------------------------------- | ---------------------------------------------------------- | ------------------------------ | | Reads your bot's tokens or database | It runs on another machine, a microVM with its own kernel | Your app never runs the code | | Reads another channel's files | Each channel has its own sandbox | One name per team and channel | | Calls out to the internet | Every outbound connection refused, root included | `network: { internet: false }` | | Loops forever | The command returns `timedOut: true` after 20 seconds | `timeoutMs` | | Keeps the machine busy overnight | It pauses after two idle minutes; CPU billing stops | `idlePauseSeconds` | | Runs up a bill | Creates past the key's daily limit fail and charge nothing | A daily spending limit per key | Your bot's Slack token stays in your app. The sandbox never needs it, because the reply is posted from your process, not from the snippet. ## Let a channel install packages The default image already has NumPy, pandas and matplotlib. For anything else, open PyPI for one install, then close it again: ```ts check import { Sandbox } from "withruntime"; const sbx = await Sandbox.getOrCreate("slack-T123-C456", { network: { internet: false } }); await sbx.network.set({ internet: true, allow: ["pypi.org", "*.pythonhosted.org"] }); await sbx.exec(["pip", "install", "--quiet", "tabulate"], { check: true, timeoutMs: 120_000 }); await sbx.network.set({ internet: false }); ``` The package stays in that channel's sandbox across pauses. Map a `/run-install tabulate` command to this, and check the package name against an allowed list before you pass it on. ## Clean up channels nobody uses Every channel sandbox carries the label `app: slack-runner`. List them and stop the ones you no longer need: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const archived = new Set(["slack-T123-C0OLD"]); // channels your app saw archived const page = await runtime.sandboxes.list({ labels: { app: "slack-runner" }, state: ["paused"] }); for await (const sbx of page) if (archived.has(sbx.info.name ?? "")) await sbx.stop(); ``` A paused sandbox is kept for 30 days by default on paid credit, and 1 to 365 days if you set it. ## What it costs Take 20,000 snippets a month. Each keeps its channel's 1 vCPU, 2 GiB sandbox running for 150 seconds, the run plus two idle minutes before the pause, and uses 2 CPU-seconds. Fifty channels each keep 0.3 GB of their own while paused: ``` CPU: 20,000 × 150 s × 0.05 vCPU / 3,600 × $0.025 = $1.04 Memory: 20,000 × 150 s / 3,600 × 2 GiB × $0.0075 = $12.50 Paused: 50 channels × 0.3 GB × $0.08 = $1.20 Total: $14.74 ``` The CPU line uses the floor: 2 CPU-seconds is less than a twentieth of a vCPU over 150 seconds (7.5 CPU-seconds), so the floor sets the charge. Counting each snippet's running time separately is an upper bound, since snippets in a busy channel share one wake. New accounts get 50 free sandbox hours, no card. ## Start ```bash no-run npx withruntime sandbox run --trial -- python3 -c 'print(6 * 7)' ``` The first run prints a link to approve in your browser. On a server, give the bot its own key from [API keys](https://withruntime.com/account/keys) in `RUNTIME_API_KEY`, with a daily limit ([security](/docs/security)). Related: [a Discord bot that runs code](/use-cases/discord-bot-code-execution), [code interpreter for chatbots](/use-cases/code-interpreter-for-chatbots), [run untrusted LLM code safely](/use-cases/run-untrusted-llm-code), [pause and resume a sandbox](/how-to/pause-and-resume-a-sandbox). ## Sources Checked 25 September 2026. - [Slack, implementing slash commands](https://docs.slack.dev/interactivity/implementing-slash-commands) - [Slack, handling user interaction](https://docs.slack.dev/interactivity/handling-user-interaction) Facts on this page were checked on 25 September 2026.