Skip to main content

Backend runnables

Backend runnables are scripts that run on Windmill workers and are called from your frontend code via the wmill.ts module. They live in the backend/ folder of your full-code app.

File structure​

Each runnable consists of up to three files in the backend/ directory:

backend/
├── get_users.ts # Code file (required)
├── get_users.yaml # Config file (optional)
├── get_users.lock # Lock file (auto-generated)
├── run_query.pg.sql # SQL runnable (language from extension)
└── run_query.lock

The runnable ID is derived from the filename: get_users.ts → runnable ID get_users.

Code-only runnables (auto-detect)​

The simplest way to define a runnable is to place a code file in backend/. The language is detected from the file extension and the runnable is treated as an inline script:

backend/get_users.ts → TypeScript (Bun) inline script, ID: "get_users"
backend/fetch_data.py → Python inline script, ID: "fetch_data"
backend/query.pg.sql → PostgreSQL inline script, ID: "query"

No .yaml file is needed for code-only runnables.

Explicit YAML config​

For more control, create a .yaml file alongside the code file:

Inline scripts​

# backend/get_users.yaml
type: inline
fields:
workspace_id:
type: static
value: "my_workspace"

The code is in the sibling file (get_users.ts, get_users.py, etc.).

Path-based runnables​

Reference an existing workspace script or flow:

# backend/process_order.yaml
type: script
path: f/production/process_order
# backend/onboarding.yaml
type: flow
path: f/flows/user_onboarding
# backend/send_email.yaml
type: hubscript
path: hub/123/sendgrid/send_email

Hub flows from the Windmill Hub can also be selected as backend runnables from the flow picker in the UI.

Saved AI agents​

An agent runnable calls a saved AI agent. Pick it under "AI agents" in the runnable picker, or reference its path:

# backend/chat.yaml
type: agent
path: f/support/agent

Each call runs one turn of the agent as the app, so in publisher execution mode viewers need no access to the agent, and a sandboxed app needs no frontend API access for it. Its inputs are user_message and optional user_attachments. Anonymous visitors cannot attach files, and on Enterprise Edition a viewer attaches only files they can read. An agent that keeps no managed memory also takes previous_messages, the history the app keeps itself. For an agent with managed memory, pass a conversation id as the memoryId option to make a call a turn of that conversation, backendAsync.chat({ user_message }, { memoryId }); without it, the call is a single run.

For a chat UI, pass the runnable's ID and the app's generated wmill.ts to the @windmill-labs/chat-sdk package:

import { useWindmillChat } from '@windmill-labs/chat-sdk/react';
import * as wmill from './wmill';

const chat = useWindmillChat({ wmill, runnable: 'chat' });

This works the same in the deployed app and under wmill app dev, but not with the AI SDK transport. The chat expects an agent with managed memory or a flow in chat mode; with memory off, every turn is answered on its own. It keeps its history in the browser and sends no attachments.

Static fields​

You can pre-fill runnable inputs with static values so the frontend doesn't need to pass them:

# backend/get_users.yaml
type: inline
fields:
limit:
type: static
value: 100
workspace:
type: static
value: "production"

Field types:

TypeDescription
staticA fixed value, set in the YAML
ctxA value from the app context (e.g., user email), see context fields
userProvided by the frontend at call time

Context fields​

A context field is filled by the server from the viewer's session, so the frontend cannot set or override it. Use one whenever a runnable needs to know who is viewing the app, for example to filter rows per viewer:

# backend/my_orders.yaml
type: inline
fields:
username:
ctx: username
-- backend/my_orders.duckdb.sql
-- $username (text)
ATTACH 'datatable' AS dt;
SELECT * FROM dt.orders WHERE owner = $username;

The available properties are username, email and groups of the viewer, workspace, and author (the email the app runs on behalf of). The viewer properties are null for a viewer who is not logged in on a public app.

An argument left as a frontend input takes whatever the browser sends, so a viewer filter declared that way lets any viewer read someone else's rows. wmill app lint and the Inputs tab of the app editor warn when an inline SQL runnable has an argument named username, email or groups that is neither a context nor a static field. The check matches on those names only: it does not catch a filter named otherwise (user_id) or one in a non-SQL runnable.

Read data as the viewer​

A backend runnable reads data as the identity the app runs as, the publisher in publisher execution mode, the same for every viewer. An inline runnable can instead read data as the logged-in person using the app, so that what it returns differs per viewer:

Resources, variables and everything else the runnable reaches stay the app's. A viewer who is not logged in, on a public app, is refused. Both data table roles and object storage permission rules are Enterprise Edition features, and data table roles are only available on self-hosted instances.

It applies to inline runnables only: a path-based runnable is someone else's script, and always reads as the app. In the app editor, turn on Read data as the viewer from the button in the inline runnable's toolbar, shown once its code reads a data table or object storage. With the CLI, set it in the runnable's YAML file:

# backend/get_orders.yaml
type: inline
viewer_data_access: true

The AI chat can also turn it on when it creates or edits a runnable.

Supported languages​

Backend runnables support all Windmill script languages:

LanguageExtension
TypeScript (Bun).bun.ts or .ts
TypeScript (Deno).deno.ts
Python.py
Bash.sh
Go.go
REST.native.ts
PostgreSQL.pg.sql
MySQL.my.sql
BigQuery.bq.sql
Oracle Database.odb.sql
Snowflake.sf.sql
MS SQL Server.ms.sql
GraphQL.gql
PowerShell.ps1
PHP.php
Rust.rs
Ansible.playbook.yml
C#.cs
DockerDockerfile
Nu.nu
Java.java
DuckDB.duckdb.sql
Ruby.rb

Plain .ts files default to Bun. Use .deno.ts for Deno runtime. Use .native.ts for REST scripts.

Backend languages

Lock files​

Lock files (.lock) pin dependency versions for reproducible builds. Generate them with:

wmill generate-metadata

This creates a .lock file for each runnable that has dependencies (TypeScript, Python, PHP).

Example​

A TypeScript runnable that queries a database:

// backend/get_users.ts
import { Client } from "pg";

export async function main(limit: number = 10) {
const client = new Client(process.env.DATABASE_URL);
await client.connect();
const result = await client.query("SELECT * FROM users LIMIT $1", [limit]);
await client.end();
return result.rows;
}

Called from the frontend:

import { backend } from "./wmill.ts";

const users = await backend.get_users({ limit: 20 });