Core Concepts
Execution
Learn how to run commands, snippets, interactive terminals, and AI-agent tasks inside one sandbox, and how to choose the right surface for each kind of work.
Banata gives you four main ways to do work inside one sandbox:
exec()for known commandsrunCode()for short code snippetsterminal()for a live shellprompt()/promptAsync()for AI-agent-led work
The important idea is that these are not separate environments.
They all operate in the same sandbox:
- the same
/workspace - the same browser state
- the same files
- the same long-running job context
That means you can mix them.
For example:
- write an input file with
fs.write() - transform it with
runCode() - inspect the result with
exec() - ask the AI agent to open a site and continue the workflow
- collect the outputs from
/workspace
A simple mental model
Use the lightest execution surface that fits the job.
- use
exec()when you already know the exact command - use
runCode()when you want to run a short snippet directly - use
terminal()when a person needs a live shell - use
promptAsync()when the task is multi-step, open-ended, or may take a while
This keeps your workflows easier to debug and easier to reason about.
exec() for deterministic command execution
Use exec() when you already know what you want to run.
const result = await sandbox.exec("sh", [
"-lc",
"python --version && node --version && git --version",
]);
console.log(result.stdout);exec() is best when you want:
- a known command
- a known tool
- a short deterministic operation
- a quick environment check
Common uses:
- check installed tools
- run one script
- read a file through shell tools
- call
python,node,go,git,curl, or other known binaries
If you already know the command, prefer exec() over the AI agent.
It is simpler, faster, and easier to debug.
runCode() for short snippets
Use runCode() when the task is still deterministic, but writing a full file first would just add friction.
const result = await sandbox.runCode(`
import { readFileSync, writeFileSync } from "node:fs";
const input = readFileSync("/workspace/input.txt", "utf8");
writeFileSync("/workspace/output.txt", input.toUpperCase());
console.log(input);
`, {
language: "typescript",
});This is useful for:
- lightweight parsing
- short transforms
- validation
- generating one derived file
- quick data cleanup
Why runCode() exists if exec() already exists
You could always create a file yourself and then call it through exec().
But runCode() removes that boilerplate for short snippets.
Instead of:
- create a temporary file
- choose a file name and extension
- execute it manually
you just:
- send the snippet
- choose the language
- get the result back
That makes runCode() a good middle ground between:
- low-level command execution
- full AI-agent orchestration
Language support in runCode()
Banata sandboxes currently support these runCode() languages:
javascripttypescriptnodebunpythonbashshgorubyrustelixirjavadeno
Example:
await sandbox.runCode('print("hello from python")', {
language: "python",
});If you do not pass a language, Banata falls back to the sandbox’s default code runner.
For predictable behavior, it is better to pass the language explicitly.
Language examples
TypeScript with Bun:
await sandbox.runCode(`
const values = [1, 2, 3];
console.log(values.map((v) => v * 2).join(","));
`, {
language: "typescript",
});JavaScript with Node:
await sandbox.runCode(`
const values = ["a", "b", "c"];
console.log(values.join("-"));
`, {
language: "node",
});Python:
await sandbox.runCode(`
from pathlib import Path
text = Path("/workspace/input.txt").read_text()
Path("/workspace/output.txt").write_text(text.upper())
print(text)
`, {
language: "python",
});Go:
await sandbox.runCode(`
package main
import "fmt"
func main() {
fmt.Println("hello from go")
}
`, {
language: "go",
});Ruby:
await sandbox.runCode(`
puts "hello from ruby"
`, {
language: "ruby",
});Rust:
await sandbox.runCode(`
fn main() {
println!("hello from rust");
}
`, {
language: "rust",
});Elixir:
await sandbox.runCode("""
IO.puts("hello from elixir")
""", {
language: "elixir",
});Java:
await sandbox.runCode(`
public class Main {
public static void main(String[] args) {
System.out.println("hello from java");
}
}
`, {
language: "java",
});Important for Java:
- define
class Main - Banata compiles and runs that class automatically
Deno:
await sandbox.runCode(`
console.log("hello from deno");
`, {
language: "deno",
});Shell:
await sandbox.runCode(`
echo "hello from shell"
ls -la /workspace
`, {
language: "bash",
});Verified sandbox tool availability
I verified these directly in a live sandbox on April 2, 2026.
Verified languages and runtimes:
- Node.js
- Python
- Go
- Ruby
- Rust
- Elixir
- Java
- Bun
- Deno
Verified common tools:
- Git
- curl
- wget
- vim
Verified AI and coding CLIs:
- Claude CLI
- Gemini CLI
- Codex
One important exception from the live check:
cursorwas not installed in the sandbox I tested
So you should not currently rely on cursor being present.
terminal() for a live shell
Use the terminal when a person or another external tool needs an interactive shell session.
const terminal = await sandbox.terminal();
console.log(terminal.terminalUrl);Use it when:
- a human operator needs to inspect the environment
- you want a real shell instead of one command at a time
- you are debugging a workflow interactively
terminal() is a human tool.
For backend automation, exec() and runCode() are usually better.
The AI agent for multi-step work
Use the AI agent when the workflow is not fully deterministic.
Examples:
- browse several pages and collect findings
- log in, wait for human approval, then continue
- read files, transform them, and decide what to do next
- drive the browser and filesystem together in one workflow
For quick synchronous work:
const reply = await sandbox.prompt(
"Open https://example.com and reply with the page title.",
);For production work, prefer promptAsync():
const queued = await sandbox.promptAsync(
"Research the target site, save findings to /workspace/report.md, and finish with RESULT_READY.",
{
metadata: {
userId: "user_123",
jobId: "job_456",
source: "admin-panel",
},
},
);
console.log(queued.taskId);Use promptAsync() when:
- the task may run for a while
- you do not want one request open
- you want webhook-driven completion
- the workflow spans browser, files, and multiple steps
How these surfaces work together
A strong Banata workflow often combines multiple surfaces.
Example pattern:
- launch a sandbox
- write inputs into
/workspace - run a deterministic transform with
runCode() - inspect the result with
exec() - ask the AI agent to continue the larger workflow
- read final outputs from
/workspace - save durable outputs as artifacts if needed
That pattern works well because each surface does one job well:
fs.*manages filesrunCode()handles short deterministic codeexec()handles exact commandspromptAsync()handles reasoning and multi-step automation
Same sandbox, same workspace
This matters a lot.
If you:
- write a file with
fs.write() - then call
runCode() - then call
exec()
all of that happens in the same sandbox and the same /workspace.
That means a file created by one step is available to the next step immediately.
This is how you should think about Banata:
- one sandbox is one working environment
- execution methods are different ways of operating inside that environment
Choosing the right method
| Need | Best method |
|---|---|
| run a known command | exec() |
| run a short deterministic snippet | runCode() |
| use a specific language directly | runCode(..., { language }) |
| open a live shell | terminal() |
| do multi-step reasoning across tools | promptAsync() |
Practical advice
A few rules will keep integrations clean:
- prefer
exec()when you already know the exact command - prefer
runCode()for short deterministic transforms - pass
languagetorunCode()explicitly - prefer
promptAsync()for long or open-ended jobs - keep working files in
/workspace - use artifacts for durable outputs you want another system to download later