# Callables documentation ## What is Callables? > Callables turns Python functions into web forms, schedules and run history that your whole team can use. What it is, and when to use it. Source: https://callables.app/docs Callables turns Python functions into tools your team can run. A developer adds `@deploy` to a function and pushes the repo to GitHub. Callables builds it and gives every function a web form generated from its type hints, with schedules, run history, secrets and per-function sharing. The people who run it never see code, a terminal or a traceback. ```python from typing import Literal from callables import deploy @deploy(title="Refund an order", secrets=["STRIPE_KEY"]) def refund( order_id: str, reason: Literal["Damaged", "Late", "Other"], notify_customer: bool = True, ) -> str: """Refunds the order in Stripe and records why.""" ... return f"Refunded {order_id}" ``` That function becomes a form with a text box, a dropdown and a checkbox. Its result shows as formatted text, and every run is saved with its inputs, logs and result. ### When to use Callables Use it when you have Python code that other people need to run, or that needs to run on its own: - A script you run by hand whenever someone in support, ops or finance asks for it. - Admin actions on your product (look up a user, issue a refund, change a plan) that need a safe UI instead of database access. - Reports and data pulls that should run on a schedule and keep their results. - File jobs: clean a CSV, convert a document, merge spreadsheets. - Internal tools that would otherwise need a front end, auth and hosting built from scratch. ### When not to use it - **Customer-facing apps or public APIs.** Callables is for internal tools. Only people on your team can run functions. - **Long-running services.** Each run is a short job (up to 15 minutes), not a server or a worker that stays up. - **Heavy data pipelines.** For multi-hour jobs or dependency graphs between tasks, use an orchestrator such as Airflow or Dagster. - **No-code workflows between SaaS apps.** If nobody on the team writes code, a tool like Zapier fits better. See [Callables vs Zapier](https://callables.app/blog/callables-vs-zapier). ### How it works 1. **Write a function** with type hints and `@deploy`. Install the SDK with `pip install callables`. 2. **Check it locally.** `call check` finds deploy problems, and `call local` runs the function on your machine with the same input validation as the platform. 3. **Push to GitHub.** Connect the repo once in the web app. Every push to the branch builds and deploys. 4. **Your team runs it** from a form in the web app, schedules it, or runs it from a result row in another function. ### Concepts - **Team:** your company's workspace. People join with a role: owner, admin, builder or member. See [sharing and access](https://callables.app/docs/sharing). - **Project:** one connected GitHub repo and branch. A project holds any number of functions. - **Function:** a Python function decorated with `@deploy`. Its name is its identity, so renaming it creates a new function. - **Group:** a set of related functions inside a project, shown as one section, with shared secrets. See [groups](https://callables.app/docs/groups). - **Deploy:** a build of one commit. Every push makes one, and you can roll a function back to an earlier one. - **Run:** one call of a function, with its inputs, logs, result, files and resource usage. ### Next steps - [Quickstart](https://callables.app/docs/quickstart): your first function, live in a few minutes. - [Coding agents](https://callables.app/docs/coding-agents): let Claude Code, Cursor or Codex write and check functions for you. - [Inputs](https://callables.app/docs/inputs) and [results](https://callables.app/docs/results): what each type hint and return type becomes. --- ## Quickstart > Install the SDK, write a function, connect a GitHub repo and run your first automation from the web app. Source: https://callables.app/docs/quickstart This takes about five minutes. You need Python 3.11 or newer, a GitHub account and a Callables account. [Create one free](https://callables.app/register): new teams get 30 days with no credit card. ### 1. Install the SDK In your project's virtual environment: ```bash pip install callables ``` This installs the `callables` package and the `call` command. Keep `callables` out of `requirements.txt`: the platform installs its own matching copy. ### 2. Create a function Run `call init` in the repo. It creates `automations.py` with an example function, and instructions for coding agents (see [coding agents](https://callables.app/docs/coding-agents)). Or write one yourself: ```python from typing import Literal from callables import deploy @deploy(title="Say hello") def say_hello( name: str, tone: Literal["friendly", "formal"] = "friendly", ) -> str: """Writes a short greeting.""" if tone == "formal": return f"Good day, **{name}**." return f"Hey **{name}**!" ``` Any module in the repo that imports `callables` is scanned for functions, so the file name doesn't matter. ### 3. Check and run it locally ```bash call check call say_hello --name Ann --tone formal call local say_hello -i name=Ann --json ``` `call check` lists every problem a deploy would hit, with a fix for each, and exits with code 1 if the deploy would fail. Every function is also a CLI command, with a flag per input. ### 4. Push the repo to GitHub Callables deploys a GitHub repo and branch, not a local folder. Commit and push, with `.env` files, keys and data kept out by `.gitignore`. ### 5. Connect the repo 1. In the web app, open **New project**. 2. Install the Callables GitHub App when asked, and give it access to the repo. 3. Pick the repo and branch, then **Connect and deploy**. The first deploy starts right away and takes a minute or two. You can follow the build log on the project page. ### 6. Run it Open the function in the web app, fill in the form and click **Run**. The result appears when the run finishes, and the run is saved in its history. From now on, every push to the branch deploys. If a function declares secrets, set their values on the project page (see [secrets](https://callables.app/docs/secrets)). To let teammates run it, [invite them and share the function](https://callables.app/docs/sharing). ### Next - [Inputs](https://callables.app/docs/inputs): dropdowns, dates, lists, models and file uploads from type hints. - [Results](https://callables.app/docs/results): return tables, Markdown or files. - [Schedules](https://callables.app/docs/schedules): run it every morning without anyone clicking. --- ## Coding agents > Set up Claude Code, Cursor, Codex and other agents to write, check and run Callables functions. Source: https://callables.app/docs/coding-agents Callables is built to be driven by coding agents such as Claude Code, Cursor, Codex and GitHub Copilot. Functions are plain Python, the SDK ships its own guide, and every CLI command has machine-readable output. ### Set up a project for agents Run this in the repo: ```bash pip install callables call init ``` `call init` writes: - `.claude/skills/callables/SKILL.md`: the full guide to writing Callables functions, as a Claude Code skill. - A Callables section in `AGENTS.md`, which Cursor, Codex, Copilot and most other agents read. It's also added to `CLAUDE.md` if the repo has one. - An example function in `automations.py`, unless the repo already has `@deploy` functions. Run `call init` again after upgrading the SDK to refresh the instructions. Your own files, and the rest of `AGENTS.md` and `CLAUDE.md`, are left alone. ### The loop an agent follows 1. Write or change a function decorated with `@deploy`. 2. Run `call check --json`. It prints every problem a deploy would hit, with a fix for each, and exits with code 1 if the deploy would fail. 3. Run `call local -i key=value --json` to try the function with real inputs. It prints one JSON object with `ok`, `value`, `files`, `error` and `field_errors`. The function's own prints go to stderr. 4. Commit and push. Every push to the connected branch deploys. ### A prompt to start with Paste this into your agent, with your own script in place of the example: ```text Turn scripts/refund.py into a Callables function my support team can run from a web form. Read the Callables guide first (call docs, or https://callables.app/llms.txt). Use Literal for fixed choices, declare API keys as secrets, return a table or Markdown, and run call check and call local before you finish. ``` ### Docs for agents These docs are published in formats agents read directly: - [/llms.txt](https://callables.app/llms.txt): an index of every page, with a one-line summary each. - [/llms-full.txt](https://callables.app/llms-full.txt): every page in one Markdown file. - Any page as Markdown: add `.md` to its URL, for example [/docs/inputs.md](https://callables.app/docs/inputs.md). Requests that send `Accept: text/markdown` get Markdown from the normal URL. - `call docs` prints the guide that ships with the installed SDK, so it always matches the version in use, even offline. ### Rules agents should follow - Read secrets with `os.environ` inside the function. Never hard-code keys or take them as inputs. - Keep import-time code free of side effects: every module that uses `callables` is imported when the platform builds. - Annotate every input and the return type. The type hints are the form and the result display. - Don't add `callables` to `requirements.txt`. - Raise `ValueError` with a message written for the person running the function. They see the message, never the traceback. --- ## The @deploy decorator > Every option @deploy takes: name, title, description, secrets, timeout, schedule and next. Source: https://callables.app/docs/deploy `@deploy` marks a function as something your team can run. It works bare or with options: ```python from callables import deploy @deploy def ping() -> str: return "pong" @deploy( title="Closed deals", secrets=["CRM_TOKEN"], timeout=120, ) def closed_deals(region: str = "NA", min_value: int = 0) -> list[dict]: """Deals closed in a region, newest first. Pulls from the CRM; amounts are in USD. """ ... ``` The person running a function can't see its code. Everything they see comes from the decorator, the docstring, the parameter names and types, and the return value, so write those for them. ### Options | Option | Default | What it does | |---|---|---| | `name` | the function's name | The function's identity on the platform | | `title` | the name as a sentence | The heading on its card and page | | `description` | the docstring's first paragraph | One line about what it does | | `secrets` | none | Environment variables injected at run time | | `timeout` | `300` | Seconds before the run is stopped, up to `900` | | `schedule` | none | A cron expression in UTC, for a schedule kept in code | | `next` | none | Actions offered on each result row | ### name The function's identity. Run history, sharing and schedules belong to it. **Changing the name creates a new function**, and the old one's history and schedules stay behind. If you rename the Python function, pass the old name to keep its identity: ```python @deploy(name="closed_deals") def deals_closed_by_region(region: str = "NA") -> list[dict]: ... ``` Inside a [group](https://callables.app/docs/groups), the full name is `.`, such as `billing.refund`. ### title and description `title` defaults to the name with underscores as spaces, so `closed_deals` shows as "Closed deals". `description` defaults to the first paragraph of the docstring. Write one plain sentence about what the function does for the person running it, not how it works. ### secrets A list of environment variable names. Builders set the values in the web app, and they're injected when the function runs. Read them inside the function: ```python import os token = os.environ["CRM_TOKEN"] ``` A declared secret that nobody has set doesn't block the run. The variable is simply missing, and builders see a "missing secrets" warning. See [secrets](https://callables.app/docs/secrets). ### timeout Seconds before the run is stopped. The default is 300 (5 minutes) and the maximum is 900 (15 minutes). A higher value is capped at 900, and `call check` warns about it. ### schedule A cron expression in UTC, such as `"0 9 * * MON"`. The schedule is created on push and removed when you delete the line. Every input needs a default. You usually don't need this: anyone can add a schedule in the web app, with a time zone. See [schedules](https://callables.app/docs/schedules). ### next Actions offered on each row of the result, with their inputs filled in from the row. See [flows](https://callables.app/docs/flows). ### Errors Raise exceptions with messages written for the person running the function. They see the message, such as `ValueError: No deals found for EMEA since 2026-01-01`, but never the traceback. For bad input, raise `ValueError` and say which field to change. Builders see the full traceback on the run page. --- ## Inputs > How type hints become form fields: text, numbers, dropdowns, dates, lists, models and file uploads. Source: https://callables.app/docs/inputs A function's parameters become its form. The type hint picks the field, the default prefills it, and the parameter name becomes the label. ```python from datetime import date from typing import Literal from callables import deploy @deploy(title="Closed deals") def closed_deals( region: Literal["NA", "EMEA", "APAC"] = "NA", since: date | None = None, min_value: int = 0, include_refunds: bool = False, ) -> list[dict]: ... ``` This form has a dropdown, an optional date picker, a number field and a checkbox. ### Type hints and fields | Type hint | Form field | |---|---| | `str` | Text box | | `int`, `float` | Number | | `bool` | Checkbox | | `date`, `datetime` | Date or date-time picker | | `Literal["a", "b"]` or an `Enum` | Dropdown | | `list[str]`, `list[int]` and so on | List editor | | `X \| None = None` | Optional field | | A Pydantic model | Grouped sub-form | | `File`, or `Annotated[File, accept(".csv")]` | File upload | ### Required and optional - A parameter without a default is required. - A default makes the field optional and prefills it. - `X | None = None` is an optional field that starts empty. ### Labels Parameter names become labels: `min_value` shows as "Min value". Choose names that read well to the people running the function. ### Validation Inputs are validated and converted before your function runs, the same way in the web app, from the CLI and in `call local`. A `date` arrives as a `datetime.date`, an `Enum` as its member and a model as an instance. Invalid input is shown next to its field, and the function isn't called. ### Models Group related fields with a Pydantic model: ```python from pydantic import BaseModel from callables import deploy class Address(BaseModel): street: str city: str postcode: str @deploy(title="Update shipping address") def update_address(order_id: str, address: Address) -> str: ... ``` ### Good practice - Prefer `Literal[...]` over free text whenever the valid values are known. - Avoid `dict` inputs: they show as a raw JSON box. Use a Pydantic model instead. - Never take an API key or password as an input. Declare it as a [secret](https://callables.app/docs/secrets). ### Not supported `*args`, `**kwargs`, `Callable` and arbitrary classes. `call check` names any parameter it can't turn into a field. For file uploads, see [files](https://callables.app/docs/files). --- ## Results > How the return type picks the display: Markdown, tables with CSV download, key-value lists and files. Source: https://callables.app/docs/results The return annotation decides how a result is shown. Always annotate it. | Return annotation | Shown as | |---|---| | `-> str` | Markdown: headings, lists, links and tables work | | `-> list[dict]`, a list of models, or a DataFrame | A table with CSV download | | `-> dict` or a Pydantic model | A key-value list | | `-> File` or `-> list[File]` | Download buttons | | `-> None` | "Done" | | Missing, or anything else | Raw JSON (avoid this) | ### Tables Return a list of dicts, a list of Pydantic models or a pandas DataFrame: ```python @deploy(title="Find a customer", secrets=["STRIPE_KEY"]) def find_customer(email: str) -> list[dict]: return [ {"id": "cus_123", "name": "Ann Lee", "plan": "Pro"}, ] ``` - Keys become column headers, so use readable keys. - Keep rows flat, with scalar values. Nested values show as JSON. - Every table can be downloaded as CSV. - Rows can offer next actions. See [flows](https://callables.app/docs/flows). ### Markdown Return a `str` and it's rendered as Markdown: ```python @deploy(title="Weekly summary") def weekly_summary(region: str = "NA") -> str: total = 48200 return f"## {region}\n\nClosed this week: **${total:,}**" ``` ### Records Return a `dict` or a Pydantic model for one record, shown as labelled values. ### Files Return a `File` or a list of them, and people get download buttons. See [files](https://callables.app/docs/files). ### Logs `print()` output and `logging` show as the run's logs, live while it runs. Use them for progress, not for the result: return the result instead. --- ## Files > Take file uploads, return files to download, and save extra files from a run. Source: https://callables.app/docs/files Functions can take file uploads, return files to download, and save extra files alongside any result. ```python from typing import Annotated from callables import File, accept, deploy, save_file @deploy(title="Clean up a contact list") def clean_contacts(sheet: Annotated[File, accept(".csv")]) -> File: """Removes duplicate rows and fixes capitalisation.""" rows = sheet.read_text().splitlines() save_file(f"{len(rows)} rows in".encode(), name="summary.txt") out = "/tmp/clean.csv" ... return File(out, name=f"clean-{sheet.name}") ``` ### Uploads A `File` parameter becomes an upload field. Use `Annotated[File, accept(".csv", ".xlsx")]` to limit the file types. Your function gets a `File` with: - `.path`: where the file is on disk - `.name`: the original file name - `.size`: its size in bytes - `.read_text()` and `.read_bytes()`: its contents - `.open()`: a file object, like the built-in `open()` ### Downloads Return a `File`, or a `list[File]`, and the person gets download buttons. `File(path, name=...)` sets the name they download it as. ### Extra files `save_file(path_or_bytes, name=...)` adds a download alongside any return value, such as a table plus the spreadsheet it came from. It needs `name=` when you pass bytes. ### Locally Pass a file to `call local` with `@`. Outputs land in `./callables-output/`, which `call init` adds to `.gitignore`. ```bash call local clean_contacts -i sheet=@contacts.csv ``` ### Limits - 100 MB per upload. - 100 MB and 20 files of output per run. - Files are deleted after 24 hours. - Functions with file inputs can't be scheduled, since nobody is there to pick the file. Write temporary files under `/tmp` (up to 256 MB) or your home directory. See [builds and runtime](https://callables.app/docs/builds). --- ## Groups and local commands > Organize related functions into groups with shared secrets, and add local-only CLI commands. Source: https://callables.app/docs/groups A project can hold many functions. Put related ones in a `Group`. People see a group as one section of the project in the web app, and you run its functions as `call `. ```python from typing import Literal from callables import Group backups = Group( "backups", title="Database backups", description="Postgres dumps to object storage", secrets=["SPACES_KEY", "SPACES_SECRET"], ) @backups.deploy(title="Back up now", secrets=["DATABASE_URL"]) def run(env: Literal["prod", "staging"] = "prod", keep: int = 7) -> str: ... @backups.deploy(title="List backups") def list_all(days: int = 30) -> list[dict]: ... ``` ```bash call backups --help call backups run --help call backups run --env staging --keep 3 ``` ### Rules - A group's `secrets` go to every action in it. Each action can declare more of its own. - A group name is lowercase letters, digits, `-` or `_`, and can't be a built-in command such as `check` or `local`. - Define each group once and import it wherever you need it. - An action's full name is `.`, such as `backups.run`. Use that with `call local`. - `@group.deploy` takes the same options as [`@deploy`](https://callables.app/docs/deploy). - Moving an existing function into a group gives it a new name, so it starts with fresh run history and sharing settings. - Plain `@deploy` functions still work. They appear in the project without a section. ### Local commands Some tools only make sense on your own machine: they read local files, write reports to disk or open a browser. Write them with `@command` (or `@group.command`) instead of `@deploy`, so the project has one CLI: ```python from callables import Group audits = Group("audits", title="Audits") @audits.command def render(slug: str, open_browser: bool = False) -> str: ... ``` - They run as `call audits render --slug x`, and with `call local`. `call check` lists them as "(local only)". - They're never deployed. Nobody can run them from the web app. - `@command` takes `name`, `title`, `description` and `next`, but no `secrets`, `schedule` or `timeout`. Read secrets from your own `.env`. - Their module is still imported when the platform builds, so keep imports that only work locally inside the function. - Return the result instead of printing it. The CLI prints what you return: aligned columns in a terminal, JSON when piped. - To deploy one later, change `@command` to `@deploy`. It keeps its name. --- ## Flows > Offer next actions on each result row, with their inputs filled in from that row. Source: https://callables.app/docs/flows A flow chains functions together. Each row of a table result, or a single record result, offers next actions with their inputs filled in from that row. Every step is still an ordinary function: it can be run on its own, scheduled or shared separately. ```python from typing import Literal from callables import Group, Next users = Group("users", title="Users") @users.deploy(title="Make admin") def make_admin(user_id: int) -> str: ... @users.deploy(title="Subscription requirement") def set_subscription( user_id: int, required: Literal["Required", "Not required"], ) -> str: ... @users.deploy( title="Find user", next=[ make_admin, Next(set_subscription, label="Change subscription"), ], ) def search(query: str) -> list[dict]: return [{"user_id": 42, "name": "Ann Lee"}] ``` ### How inputs are filled - An action's inputs are filled from result fields with the same name. - `Next(action, user_id="id")` fills `user_id` from a field with a different name, here `id`. - `label=` changes the button text. - Inputs the row doesn't fill are left for the person to complete. ### What next accepts - Deployed functions, as in `next=[make_admin]`. - Names, for a function defined further down: `"make_admin"`, or `"users.make_admin"` for another group's action. - `Next(...)`, for a label or field mapping. ### In the web app Each result row gets a button per next action. It opens that action's form with the row's values filled in, and the new run links back to the one it came from. People only see next actions they're allowed to run, so you can let a whole team search users and only a few people make admins. See [sharing and access](https://callables.app/docs/sharing). ### In a terminal `call users search --query ann` lists the rows, asks which one and what to do next, prompts for any inputs the row didn't fill, and runs it. Press Enter to stop. Scripts and `--json` get plain output with no prompts. For a worked example, see [build an internal admin panel in Python](https://callables.app/blog/internal-admin-panel-in-python). --- ## Secrets > Give functions API keys and passwords without putting them in code or showing them to the people who run them. Source: https://callables.app/docs/secrets Secrets give functions API keys, passwords and tokens without putting them in code or showing them to the people who run them. ### Declare what a function needs List the environment variables in `secrets`, and read them inside the function: ```python import os import stripe from callables import deploy @deploy(title="Refund a charge", secrets=["STRIPE_KEY"]) def refund(charge_id: str) -> str: stripe.api_key = os.environ["STRIPE_KEY"] stripe.Refund.create(charge=charge_id) return f"Refunded {charge_id}." ``` The code only names the secret. The value never appears in the repo. ### Set the value After you push, set the value in the web app. Values are stored encrypted and injected as environment variables each time the function runs. A function only receives the secrets it declares. Each secret can be set at one of three levels: | Level | Applies to | |---|---| | Team | Every function on the team | | Project | Every function in one project | | Function | One function | When the same name is set at more than one level, the most specific wins: function, then project, then team. Builders set and change secrets. People who only run a function never see the values. ### Missing secrets A declared secret that nobody has set doesn't block the run. The variable is simply missing, and builders see a "missing secrets" warning on the project. For an optional secret, read it with `os.environ.get("NAME")` and handle it being missing. ### Groups A [group](https://callables.app/docs/groups) can declare secrets once for every action in it: ```python from callables import Group billing = Group("billing", title="Billing", secrets=["STRIPE_KEY"]) ``` ### Key files Each secret is a single value. For a `.pem` file or a service account JSON, paste the file's contents as the secret, and have your code read it from the variable. ### Locally `call local` and `call ` read the same variables from your environment, so a `.env` file works well. Keep it in `.gitignore`. ### Rules - Never hard-code a key, even for testing. - Never take a key as a form input. - Read secrets inside the function, never at module level: modules are imported at build time, when secrets aren't set. - Give each function the narrowest key that works, such as a restricted Stripe key. --- ## Schedules > Run functions on a schedule from the web app, or declare the schedule in code. Source: https://callables.app/docs/schedules Any function can run on a schedule. Each scheduled run is saved in the run history like any other, with its inputs, logs and result. ### From the web app Open the function, fill in its form the way it should run, then click **Schedule…** next to Run. Choose: - Every hour, day, weekday, week (on the days you choose) or month - Or any cron expression - A time zone, so 9am stays 9am when the clocks change You see the next few run times as you edit. Each schedule keeps its own inputs and an optional name, so one function can run every Monday for NA and every Tuesday for EMEA. A toggle pauses a schedule without deleting it. ### In code For a schedule that should always exist with the code, such as a nightly cleanup: ```python # Every day at 2am UTC @deploy(title="Nightly cleanup", schedule="0 2 * * *") def nightly_cleanup(dry_run: bool = False) -> str: ... ``` It's created when you push. Changing or removing the line updates or removes it on the next push. Code schedules use UTC. ### Rules - **Every input needs a value.** A schedule runs with the inputs saved on it. Code schedules need a default for every input. - **Functions that take a file upload can't be scheduled.** - **Schedules run as the person who made them,** with their access. A schedule stops if that person can no longer run the function, so someone leaving the team doesn't leave jobs running under their name. - People who manage a function see all of its schedules. --- ## Sharing and access > Team roles, who can run each function, and who sees which runs. Source: https://callables.app/docs/sharing Callables decides who can run each function, and keeps each person's runs private by default. ### Roles Everyone on a team has one role: | Role | Can | |---|---| | Owner | Everything, including deleting the team and managing other owners | | Admin | Manage the team and its people, but not delete it or change owners | | Builder | Connect repos, deploy, and manage the functions they deploy | | Member | Run the functions shared with them | Most people in a company are members: the people in support, ops or finance who run tools but don't write them. ### Sharing a function A newly deployed function is private. Only the builder who deployed it, and owners and admins, can run it. In the function's sharing settings, either: - **Share it with the whole team**, for tools anyone can use, or - **Add people** with **Can run** (see and run it) or **Can manage** (also change its sharing, and see all its runs and schedules). Builders don't automatically see each other's functions, so a payroll tool isn't visible to every engineer who can deploy. ### Who sees which runs - Everyone sees their own runs. - People who manage a function see all of its runs. - Owners and admins see every run on the team. ### Flows and secrets - In a [flow](https://callables.app/docs/flows), people only see next actions they're allowed to run. - People who run a function never see its [secrets](https://callables.app/docs/secrets). - Tracebacks are shown to builders only. People running a function see the error message. ### Inviting people Invite people by email or with an invite link, and pick their role when you invite them. On Pro, you can add a trusted email domain, so anyone with a company email address can join. --- ## Builds and runtime > How a push becomes a deploy: Python version, dependencies, system packages, subfolders and the sandbox each run gets. Source: https://callables.app/docs/builds Every push to a connected branch builds the repo into an image and deploys every `@deploy` function in it. You can follow each build's log on the project page, and roll a function back to an earlier deploy. ### What a build does 1. Clones the commit. 2. Installs system packages from `apt.txt`, if there is one. 3. Installs Python and your dependencies. 4. Imports every module that uses `callables`, and reads the functions and their type hints. 5. Publishes the functions. If any module fails to import, nothing is published and the previous deploy stays live. Run `call check` before pushing to catch the problems a build would hit. ### Python version Taken from `.python-version` (such as `3.13`). Without one, the newest version your `pyproject.toml` `requires-python` allows. Otherwise 3.12. Supported: 3.11 to 3.14. ### Python dependencies From `requirements.txt` at the repo root, or `[project].dependencies` in `pyproject.toml`. Don't list `callables`: the platform installs its own matching copy. ### System packages When a function shells out to a command-line tool, list the Debian packages it needs in `apt.txt` at the repo root, one per line: ```text # apt.txt ffmpeg poppler-utils ``` - Names only, no versions. You get the version Debian 13 (trixie) ships. - Packages are installed without their recommended extras. If a tool is missing something, such as fonts, list that package too. - They're installed once per change to `apt.txt`, not on every push. - `call local` uses the tools on your own machine, so install them there too. ### Functions in a subfolder By default the whole repo is built. To build one folder, such as in a monorepo, add `callables.toml` at the repo root: ```toml root = "automations" ``` Or `[tool.callables]` with `root = "automations"` in `pyproject.toml`. Only that folder goes into the image, and its own `requirements.txt`, `apt.txt` and `.python-version` are used. Code in it can't import from the rest of the repo. `call check`, `call local` and `call manifest` use the same folder when run from the repo root. ### Import-time code Every module that imports `callables` is imported at build time. Keep import-time code cheap and free of side effects: no network calls, and no reading secrets at module level. Read secrets inside the function. ### The run sandbox - Each run gets a fresh sandbox on Debian 13 (trixie), as a non-root user, with internet access. - Your code in `/app` is read-only. - You can write to your home directory (`~`) and to `/tmp`, which is limited to 256 MB. - Nothing persists between runs. Keep state in your own database or storage. - `/tmp` doesn't allow running programs. Put a downloaded binary under `~` and `chmod +x` it, or better, install it with `apt.txt`. - `print()` and `logging` output show as the run's logs. ### Memory and CPU You don't size anything. A run can use memory and CPU up to the team's plan limit, and usage is billed on what the run actually used. Builders see each run's peak memory and CPU time on its page. A run that goes over the memory limit fails with an error that says so: process data in chunks, or move to a bigger plan. Child processes count toward the same limit. ### Rollback Each function page lists its recent deploys. Roll back to any version that's still kept: the current one, the three before it, and any that a queued or running run needs. --- ## Limits > Run length, file sizes, memory and the other limits that apply to every run. Source: https://callables.app/docs/limits These limits apply to every run on every plan unless noted. | Limit | Value | |---|---| | Run length | 5 minutes by default, up to 15 minutes with `timeout=900` | | Upload size | 100 MB per file | | Output files | 100 MB and 20 files per run | | File retention | Uploads and outputs are deleted after 24 hours | | Writable `/tmp` | 256 MB | | Runs at once | 5 per team; more wait in a queue | | Memory and CPU per run | Set by the plan. See [pricing](https://callables.app/pricing) | | Projects and people | Set by the plan. See [pricing](https://callables.app/pricing) | | Python | 3.11 to 3.14 | | Operating system | Debian 13 (trixie) | ### When a run hits a limit - **Too long:** the run is stopped with "Stopped after the 300s time limit" (or whatever its timeout is). - **Too much memory:** the run fails with an error saying it ran out of memory on your plan. - **Too many or too large output files:** the run still finishes. Files past the limit are dropped, with a warning in its logs. - **Too many runs at once:** new runs wait in the queue and start when one finishes. See [builds and runtime](https://callables.app/docs/builds) for the sandbox each run gets. --- ## CLI reference > Every call command: check, local, run as a command, manifest, docs and init. Source: https://callables.app/docs/cli `pip install callables` installs the `call` command. Run it from the repo root. The old `shipfn` command is an alias. ### call check ```bash call check call check --json ``` Lists every problem a deploy would hit, with a fix for each. Exit code 1 means the deploy would fail. `--json` prints the issues as JSON for scripts and agents. ### call ```bash call weekly_report --region EMEA --days 30 call backups run --env staging call backups run --help ``` Every function is a CLI command, and every group a sub-command. Each input is a flag: `days` becomes `--days`, a bool becomes `--dry-run/--no-dry-run`, and a list takes several values. Add `--help` to see them. Results print as aligned columns in a terminal and as JSON when piped. ### call local ```bash call local weekly_report -i region=EMEA -i days=30 call local clean_contacts -i sheet=@contacts.csv call local backups.run -i env=staging --json ``` Runs a function on this machine with the same input validation as the platform. Values are parsed as JSON when possible, and `@path` passes a file. Output files land in `./callables-output/`. `--json` prints one JSON object on stdout with `ok`, `value`, `files`, `error` and `field_errors`, and the function's own prints go to stderr. ### call manifest ```bash call manifest ``` Prints the JSON the platform reads: every function, its options and each input's JSON Schema. Local-only commands are left out. ### call docs Prints the guide to writing functions that ships with the installed SDK. ### call init ```bash call init ``` Sets up a repo: an example function, a `requirements.txt` if there's none, `callables-output/` in `.gitignore`, and instructions for coding agents. See [coding agents](https://callables.app/docs/coding-agents). Safe to run again: it only refreshes the files it owns.