Skip to main content
The capsule.commands interface gives you full control over process execution inside a capsule. You can run commands synchronously and wait for the result, launch long-running processes in the background, or stream output in real time over a WebSocket connection.

Foreground execution

capsule.commands.run() executes a shell command and blocks until it exits. It returns a CommandResult with the full stdout, stderr, exit code, and wall-clock duration.
result = capsule.commands.run("python -c 'print(42)'")
print(result.stdout)       # "42\n"
print(result.stderr)       # ""
print(result.exit_code)    # 0
print(result.duration_ms)  # 35

Options

ParameterTypeDefaultDescription
timeoutint | None30Seconds before the command is killed. None disables the timeout.
envsdict[str, str] | NoneNoneAdditional environment variables for the process.
cwdstr | NoneNoneWorking directory for the process.
result = capsule.commands.run(
    "python train.py",
    timeout=300,
    envs={"CUDA_VISIBLE_DEVICES": "0", "LOG_LEVEL": "debug"},
    cwd="/app",
)

Background execution

Pass background=True to launch a process without waiting for it to finish. You get a CommandHandle immediately with the process PID and a tag.
handle = capsule.commands.run("python server.py", background=True)
print(handle.pid)  # 1234
print(handle.tag)  # "exec-abc123"
Pass a tag string to label a background process with a human-readable name. You can use the same tag later to find it in capsule.commands.list().
handle = capsule.commands.run(
    "uvicorn app:main --port 8000",
    background=True,
    tag="api-server",
)

Streaming execution

capsule.commands.stream() opens a WebSocket connection and yields events as the process runs. Use this when you want to display output in real time instead of waiting for the process to finish.
import sys

for event in capsule.commands.stream("python", args=["-u", "train.py"]):
    match event.type:
        case "start":
            print(f"process started with PID {event.pid}")
        case "stdout":
            print(event.data, end="")
        case "stderr":
            print(event.data, end="", file=sys.stderr)
        case "exit":
            print(f"\nexited with code {event.exit_code}")
When you pass args, the command is executed directly. When you omit args, the command string is passed to /bin/sh -c.

Stream event types

Event typeFieldsDescription
startpidProcess has started.
stdoutdataA chunk of standard output.
stderrdataA chunk of standard error.
exitexit_codeProcess has exited.
errordataA stream-level error occurred.

Connecting to a running background process

capsule.commands.connect() attaches to an already-running background process by PID and yields its output events, exactly like stream().
# Launch a background process
handle = capsule.commands.run("python long_job.py", background=True)

# Attach to it later and stream its output
for event in capsule.commands.connect(handle.pid):
    if event.type == "stdout":
        print(event.data, end="")
    elif event.type == "exit":
        print(f"exited: {event.exit_code}")

Listing running processes

capsule.commands.list() returns all background processes currently running in the capsule as a list of ProcessInfo objects.
for proc in capsule.commands.list():
    print(proc.pid, proc.tag, proc.cmd)
Each ProcessInfo has:
FieldTypeDescription
pidintProcess PID.
tagstr | NoneUser-assigned or auto-generated tag.
cmdstr | NoneExecutable path.
argslist[str] | NoneArguments passed to the executable.

Killing a process

Send SIGKILL to a background process by PID:
capsule.commands.kill(pid=1234)

Complete example

from wrenn import Capsule
import sys

with Capsule(template="minimal", wait=True) as capsule:
    # Write a script to run
    capsule.files.write("/app/task.py", """
import time
for i in range(5):
    print(f"step {i}")
    time.sleep(0.5)
""")

    # Stream it
    for event in capsule.commands.stream("python", args=["-u", "/app/task.py"]):
        match event.type:
            case "stdout":
                sys.stdout.write(event.data)
            case "exit":
                print(f"done (exit {event.exit_code})")

    # Launch a background server
    handle = capsule.commands.run(
        "python -m http.server 8080",
        background=True,
        cwd="/app",
        tag="http-server",
    )

    # Confirm it is running
    procs = capsule.commands.list()
    print(any(p.tag == "http-server" for p in procs))  # True

    # Kill it
    capsule.commands.kill(pid=handle.pid)