Skip to main content

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 isTypical time and cost
CodePython: 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 codeNotes
semantic.query (Run saved queries)run declared semantic queriesThe 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.writeread and write its own small cachenamed keys, a maximum age
blob.read / blob.writeread and write named filesa workflow's files when called from a workflow (used for trained models)
llm.call (Ask AI)call a model from the workspace's allowed listat most 50 calls and $5 per run
fn.callcall other functionsat 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_once or fail). Older drafts that set a temperature should use effort instead.
  • 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.

Next​