Workspaces
Workspaces are reactive Python notebooks that an AI agent or a user drives. An agent calls the run_notebook MCP tool to spin up a sandboxed, git-backed workspace. Inside the notebook, the Data SDK (import signalpilot as sp) queries the warehouse through the same governed gateway used by every other SignalPilot tool: authenticated, LIMIT-injected, and audit-logged.
Notebooks are reactive: cells form a DAG, and changing one cell re-runs its dependents. The same notebook can be built headless by an agent over MCP or opened and edited by a user in the web UI.
The MCP tools
Two tools cover the agent workflow.
list_workspace_projects
No parameters. Lists active workspace projects with their id, display name, source (managed or github), default branch, and file count. Use the returned id to pick where to work.
Tool: list_workspace_projects→ Found 2 workspace project(s):- analytics-nb (id=wsp_abc123) branch=main files=4- github-mirror (id=wsp_def456) branch=main files=12 [github]remote: https://github.com/acme/analytics
run_notebook
Writes a .py notebook file into the workspace and executes it in an isolated sandbox. Returns cell outputs and a URL to view the notebook in the browser.
| Param | Type | Required | Description |
|---|---|---|---|
filename | string | yes | Name of the .py file, e.g. analysis.py. Must end in .py and be a relative path inside the workspace. |
code | string | yes | Full contents of the .py notebook file. |
The first call creates the sandbox and workspace. Later calls reuse the running sandbox for the same org/user, so sandbox-local state persists across calls within a session. Each execution runs sp export session on the file and commits the result to the workspace git repo.
Tool: run_notebookfilename = "analysis.py"code = "<full .py notebook file>"→ Notebook executed successfully.--- Cell Outputs ---[Cell 0] ...notebook_url: https://app.signalpilot.ai/notebooks?file=analysis.py&session_id=...
Notebook format
A notebook is a plain .py file. Each cell is an @app.cell function; the return tuple shares variables with downstream cells, and the last expression before return renders as the cell's visual output.
import signalpilot
__generated_with = "0.13.0"
app = signalpilot.App()
@app.cell
def _():
import signalpilot as sp
sp.md("# My Analysis")
return (sp,)
@app.cell
def _(sp):
sp.init()
conn = sp.connect("my_connection")
df = conn.query("SELECT * FROM orders LIMIT 100")
df
return (conn, df,)
@app.cell
def _(df):
print(f"Rows: {len(df)}")
return ()
if __name__ == "__main__":
app.run()
Cell rules:
- One definition per name. Defining
dfin two cells is a hard error. Use a_prefix (_tmp) for cell-local temporaries. - The last expression before
returnis the cell's single visual output. A baredfrenders as a table;sp.md(...)renders markdown. - A cell shows exactly one output. To show several things, wrap them with
sp.vstack([...])orsp.hstack([...]). print()goes to the console log; it appears in the MCP agent response, not in the cell's visual output.
Data SDK quickstart
The notebook talks to the warehouse through the gateway via the signalpilot SDK. The first cell imports it; data calls require sp.init() first. In cloud/MCP mode sp.init() auto-detects the gateway URL and session token. No manual config.
import signalpilot as sp
sp.init() # REQUIRED before any data call
conns = sp.connections() # list available connections
db = sp.connect("connection_name") # get a connection
rows = db.query("SELECT * FROM users LIMIT 10") # returns list of dicts
Schema exploration:
db.tables() # list all tables
db.tables(filter="user") # filter by name
db.describe("users") # column details
db.schema_overview() # high-level summary
Query analysis and SQL cells:
db.explain("SELECT ...") # execution plan
db.sample_values("users", "country") # sample distinct values
db.join_path("orders", "customers") # find join paths
result = sp.sql("SELECT * FROM t", engine=db) # returns a pandas DataFrame
UI widgets are reactive: changing one re-runs dependent cells. Each must be the cell's last expression (or inside sp.vstack) to render:
slider = sp.ui.slider(start=0, stop=100, value=50, label="X")
dropdown = sp.ui.dropdown(["A", "B", "C"], label="Pick")
table = sp.ui.table(df) # interactive data table
explorer = sp.ui.dataframe(df) # full filter/sort/search explorer
Pre-installed packages in the sandbox: pandas, polars, numpy, duckdb, sqlglot, altair, plotly, matplotlib, seaborn, scikit-learn, scipy, pyarrow, dbt-core, dbt-duckdb, anthropic, openai, mcp.
Isolation
Each session runs in its own isolated microVM, one per org/user. Isolation is enforced at several layers:
- One microVM per session. Nothing is shared with other sessions or with the host.
- Restricted egress. A sandbox can reach DNS, the gateway, and the hosts in the
SP_NOTEBOOK_EGRESS_ALLOWallowlist. Nothing else. - Fixed resources. CPU and memory are fixed per sandbox (
SP_NOTEBOOK_VCPUS,SP_NOTEBOOK_MEMORY_MB), andSP_NOTEBOOK_MAX_RUNNING_PER_ORG(default 20) caps how many sandboxes one organization can run at once. - Git-backed workspace. The workspace lives in a git repo. Each
run_notebookexecution commits its result, so notebook history is versioned. - Snapshot and resume. A sandbox stays warm across
run_notebookcalls for fast iteration. AfterSP_NOTEBOOK_IDLE_SNAPSHOT_SECONDS(15 minutes) of inactivity it is snapshotted and destroyed. The next call resumes from the snapshot, which is kept forSP_NOTEBOOK_SNAPSHOT_EXPIRATION_SECONDS(7 days). Durable state is the committed workspace git repo, not the sandbox filesystem.
Opening and editing in the web UI
run_notebook returns a notebook_url pointing at the web app (e.g. https://app.signalpilot.ai/notebooks?file=analysis.py&session_id=...). Open it to view and edit the notebook interactively. The same reactive engine runs in the browser, so editing a cell re-runs its dependents live. Notebooks an agent builds headless and notebooks a user edits in the UI are the same files in the same git-backed workspace.
Notebooks live in the workspace under <project>/notebooks/, not in the project root or models/.