Apps — build once, use again
The agent writes a small program into the workspace, wires resources into it, and runs it — today and next week. Blueprints, bindings, artifacts and the authority model.
An agent that answers a question leaves you with an answer. An agent that builds an app leaves you with something you can run again tomorrow, with different input, without a model in the loop.
agent.run(
"Create an app named revenue_by_region that totals `amount` per `region` "
"from a CSV, then use it on bookings.csv."
)├─ Tool group: Listed blueprints, built the app revenue_by_region, used the app
│ ├─ list_blueprints completed
│ ├─ create_app completed
│ └─ use_app completed 62ms
│
├─ Artifact: Revenue by region
│ Page · /project/.shipit/apps/revenue_page/revenue.html
│
└─ Final answer
EMEA 204,000 · AMER 494,000 · APAC 109,000.What it looks like
Next quarter, that dashboard is one store.run("revenue_dash", …) away — no
model, no tokens, same numbers.
The four tools
They ship as builtins, storing under <project>/.shipit/apps.
| Tool | What it does |
|---|---|
list_blueprints | What can I start from, and what already exists? |
create_app | Write one into the workspace — from a blueprint, from code, or both |
set_app_binding | Wire a resource into it |
use_app | Run it with input, and get its return value back |
The split is deliberate. Creating writes files; running executes code the model wrote; wiring changes what an app is allowed to reach. Those are different acts with different risks, and each gets its own row in the transcript.
What an app is
A directory with a manifest and an app.py exporting one function:
# .shipit/apps/revenue_by_region/app.py
"""Total `amount` per `region`. Expects: path (str) — the CSV to read."""
import csv
from pathlib import Path
def run(input, env):
with Path(input["path"]).open() as handle:
rows = list(csv.DictReader(handle))
totals = {}
for row in rows:
totals[row["region"]] = totals.get(row["region"], 0) + float(row["amount"])
return {"totals": totals, "grand_total": sum(totals.values())}Its module docstring is not decoration: when a run fails, use_app returns
it to the agent, so a KeyError becomes a correction rather than a guess.
Blueprints
Starting points, copied into the new app:
| Blueprint | Produces |
|---|---|
report | A Markdown table from records |
csv_summary | Row counts from a CSV, optionally grouped by a column |
page | A self-contained HTML page |
dashboard | Headline cards and a bar chart, with a share-of-total table |
sheet | A spreadsheet view — column letters, row numbers, flagged cells |
workflow | A pipeline as boxes and connectors, with a live log |
from shipit_agent.apps import AppStore
store = AppStore(".shipit/apps", workdir=".")
store.create("revenue_dash", title="Revenue by region", blueprint="dashboard")
store.run("revenue_dash", {
"rows": rows,
"title": "Revenue by region · Q2 FY2025",
"subtitle": "warehouse · fy25_bookings",
"output": "revenue.html",
})Every blueprint is self-contained: no CDN, no web fonts, no <script>,
no @import. An artifact that needs the network is not one you can email
someone, and a test asserts it. Data is escaped, so a <script> in a cell
stays text.
dashboard guesses its own label and value fields, so a model that passes
only rows still gets a chart. sheet takes
highlight={"Status": {"At risk": 1}} to tint the cells someone needs to
look at.
Running an app
result = store.run("revenue_by_region", {"path": "bookings.csv"})
result.ok # True
result.value # {"totals": {...}, "grand_total": 807000.0}
result.stdout # anything it printed
result.error # traceback, if it raised
result.env_calls # how many resource calls it madestore.run() rather than run_app(): the store knows where apps run —
the project the agent works in, not the app's own install directory. An app
given path="bookings.csv" means the file the agent has been working with.
Artifacts
An app that writes a file declares it, and the run emits an event:
{"type": "artifact_created", "path": "/project/revenue.html",
"title": "Revenue by region", "kind": "Page", "tool": "use_app"}Which every surface draws as a card — Revenue by region · Page · Click to open. Kinds come from the extension: Doc, Sheet, Page, Data,
Image, Deck, Code, PDF, Archive, File.
Two rules keep the cards honest:
- Only declared paths. Scraping them out of tool text would invent an artifact from any string with a slash.
- Only side-effecting tools.
read_filereports the path it read; treating that as an artifact would draw a card for a file you already had.
Any tool of your own gets cards for free by putting path (or paths) in
its result metadata.
Authority
An app is never more privileged than the agent that wrote it.
- It runs in a subprocess with a scrubbed environment — no credentials.
- Its
envreaches the parent over the same capability bridge code mode uses, so every resource call is gated by the permission engine, the contracts and the approval queue exactly as the equivalent tool call would be. - It sees only what it was wired:
store.bind("revenue_by_region", source="WAREHOUSE", as_name="DB")def run(input, env):
rows = env.DB.call(query="SELECT region, amount FROM bookings")An unwired binding is not merely refused — it is not in env at all. A test
asserts the app cannot even name it.
Children of an app get read-only tools by default, because a sub-agent that can write is a side effect nobody reviewed.
Running one without an agent
That is the point of building one:
from shipit_agent.apps import AppStore
store = AppStore(".shipit/apps", workdir=".")
for month in ("q2.csv", "q3.csv"):
print(month, store.run("revenue_by_region", {"path": month}).value)No LLM call, no tokens, no drift.
Contracts
| Tool | Read-only | Auto-approvable | Blocks the agent |
|---|---|---|---|
list_blueprints | yes | — | — |
create_app | no | yes (fs.write) | yes |
set_app_binding | no | yes (fs.write) | yes |
use_app | no | never | yes |
use_app runs code the model wrote, so the tag says nothing about what a
particular call does — it can never be auto-approved, and the agent always
waits for the result.
See also
- Automatic delegation
- The live UI panel — where artifact cards are drawn
- Connections — what an app can be wired to