Functions and their kinds
A function is a named, versioned unit of work with one input and one output, run by Studio for any caller (an app button, a workflow step, an agent, a coding agent over MCP, the app's chat, or a script) as one run with one trace. It declares every capability it needs; nothing undeclared is reachable.
The four kinds
| Kind (Studio name) | What it is | Typical time and cost |
|---|---|---|
| Code | Python: def handler(input, ctx) -> dict, run in a sandbox. The image ships the standard library, numpy, pandas, pyarrow, DuckDB, scikit-learn, joblib and skops; there is no pip at run time. Tests with fixtures are required. | under a second to a few seconds once warm; a cold start adds seconds; compute only, no AI cost |
Ask AI (llm) | One structured AI answer from a prompt file that reads fields of the input. You choose the model, how hard it thinks (effort: low, medium, high), the longest answer and a cost cap. | 1 to 10 seconds, cents |
Sort into categories (classify) | One text field of the input sorted into a closed list of labels you write, consistently. | under 2 seconds, a fraction of a cent |
Agent that investigates (agent) | Several AI steps and read-only lookups under limits. Always runs in the background. See Agents. | 30 seconds to minutes, cents to about a dollar |
The kind cannot change after creation; a different kind is a new function. Pick the least powerful kind that does the job: a fixed computation is Code; one AI answer is Ask AI; a label is Sort into categories; judgement over messy evidence is Agent.
A fifth kind, Lookup, is retired. Never create one; read a connection with an agent that has access to it.
What a Code function may do (capabilities)
| Capability (Studio name) | Lets the code | Notes |
|---|---|---|
semantic.query (Run saved queries) | run declared semantic queries | The query ids are named here, but the SQL is declared by the app or workflow that calls the function. The function carries no SQL of its own. |
cache.read / cache.write | read and write its own small cache | named keys, a maximum age |
blob.read / blob.write | read and write named files | a workflow's files when called from a workflow (used for trained models) |
llm.call (Ask AI) | call a model from the workspace's allowed list | at most 50 calls and $5 per run |
fn.call | call other functions | at most 3 levels deep |
Sends are never a capability: only a workflow send step changes another system.
The handler's ctx offers ctx.query(id, params), ctx.cache.get/set/delete, ctx.blob.get/put/list, ctx.llm(model, ...), ctx.call(ref, input), ctx.log, ctx.progress(pct, note), ctx.deadline_ms() and ctx.remaining_ms(). Using a capability the manifest does not grant raises capability_not_granted.
Who may see and call it (visibility)
- Audience: private (you), the app it belongs to, the whole workspace, or Bicycle's library.
- Expose to: apps, workflows, agents (and chat), MCP. Each is a switch on the function.
Turning on agents or MCP, or widening the audience, is a widening change a person confirms at publish.
Sizes, time limits and cost
- A Code function picks a size (Small, Medium, Large) and a time limit: up to 30 seconds when called synchronously, up to 15 minutes on the normal path; longer runs take the job path, which only workflows use.
- Ask AI: choose the model from the workspace catalogue,
effort(how hard it thinks; low is fastest and cheapest),max_output_tokens,max_cost_usd, and what to do if the answer does not fit the shape (repair_onceorfail). Older drafts that set atemperatureshould useeffortinstead. - Agent: steps, tool calls, cost and wall time, within the workspace caps. See Agents.
- Every run shows its cost and time on the Runs page.
A Code function, end to end
{
"schema": "bicycle.function/v1",
"name": "order_kpis",
"kind": "code",
"title": "Order KPIs for a day",
"entrypoint": "main:handler",
"image": "bda-python:3",
"mode": "async",
"timeout_ms": 10000,
"resources": { "class": "fn-xs" },
"input_schema": { "type": "object", "required": ["day"], "properties": { "day": { "type": "string" } } },
"output_schema": { "type": "object", "properties": { "orders": { "type": "number" }, "failed_pct": { "type": "number" } } },
"capabilities": [ { "id": "semantic.query", "queries": ["orders_by_day"] } ],
"visibility": { "audience": "tenant", "expose": { "apps": true, "workflows": true, "agents": false, "mcp": false } },
"tests": [ { "name": "one day", "input": { "day": "2026-09-20" },
"fixtures": { "orders_by_day": "fixtures/day.json" }, "expect": { "orders": 120 } } ]
}
# main.py
def handler(input, ctx):
res = ctx.query("orders_by_day", {"from": input["day"], "to": input["day"]})
rows = res.records()
orders = sum(r["orders"] for r in rows)
failed = sum(r["failed_orders"] for r in rows)
return {"orders": orders, "failed_pct": round(100 * failed / orders, 2) if orders else 0}
The app that calls it must declare the query orders_by_day in its manifest. The full field list of function.json is generated from the schema: function manifest fields.
An Ask AI function
function.json with "kind": "llm" and an llm block (model, prompt, optional system, effort, max_output_tokens, on_invalid, max_cost_usd); the prompt is a markdown file under prompts/ that reads input fields as {{input/order_note}}. The output is checked against output_schema; if it does not fit, one repair attempt is made. The answer is data: whatever reads it next must not treat it as instructions.
A Sort into categories function
"kind": "classify" with classify: { labels, multi_label?, instructions?, text_field }. The text is classified, never put into the instructions. Rows the classifier is unsure about stay unlabelled for a person.