Executables
Executables are the building blocks of flow automation. They can be simple commands, complex multi-step workflows, HTTP requests, or even GUI applications. This guide covers all executable types and configuration options.
Finding Executables
Use the flow browse command to discover executables across your workspaces:
flow browse # Interactive multi-pane browser
flow browse --list # Simple list view
flow browse VERB ID # View specific executable detailsFilter executables by workspace, namespace, verb, or tag:
flow browse --workspace api --namespace v1 --verb deploy --tag production
flow browse --all --filter "database" # Search names and descriptionsExecutable Configuration
Basic Structure
Every executable needs a verb and optionally a name:
executables:
- verb: run
name: my-task
description: "Does something useful"
tags: [development, automation]
aliases: [task, job]
timeout: 5m
visibility: public
exec:
cmd: echo "Hello, world!"Common Fields
- verb: Action type (run, build, test, deploy, etc.)
- verbAliases: Alternative names for the verb
- name: Unique identifier within the namespace
- description: Markdown documentation for the executable
- tags: Labels for categorization and filtering
- aliases: Alternative names for the executable
- timeout: Maximum execution time (e.g., 30s, 5m, 1h)
- visibility: Access control (public, private, internal, hidden)
Custom Verbs
If none of the built-in verbs fit, register your own:
flow config add verb statusThe verb can then be used in flow files (verb: status) and on the command line (flow status my-services). Remove one with flow config remove verb status.
NOTE
Custom verbs are stored in your user config, not the workspace. Anyone running a flow file that uses one needs to register it too.
Visibility Levels
- public: Available from any workspace
- private: Only available within the same workspace but shown in browse lists (default)
- internal: Available within workspace but hidden from browse lists
- hidden: Cannot be run or listed
Environment Variables
Customize executable behavior with environment variables or temporary files using params or args.
INFO
Executables inherit environment variables from their parent executable, workspace, and system.
By default, values defined in the .env file at the workspace root are automatically loaded. This can be overriden in the workspace configuration file with the envFiles field.
Parameters (params)
Set environment data from various sources:
executables:
- verb: deploy
name: app
exec:
file: deploy.sh
params:
# From secrets
- secretRef: api-token
envKey: API_TOKEN
- secretRef: production/database-url
envKey: DATABASE_URL
# Interactive prompts
- prompt: "Which environment?"
envKey: ENVIRONMENT
# Static values
- text: "production"
envKey: DEPLOY_ENV
# Env File (key=value format)
- envFile: "development.env"
- envFile: "staging.env"
envKey: SHARED_KEYS # Only load specific keys
# Saved to a file
- secretRef: tls-cert
outputFile: cert.pemParameter types:
secretRef: Reference to vault secretprompt: Interactive user inputtext: Static valueenvFile: Load environment variables from a file. Relative paths resolve against the directory holding the flow file (FLOW_DEFINITION_DIR), not the workspace root or your current directory — settingdiron the executable does not change this.
Arguments (args)
Handle command-line arguments:
executables:
- verb: build
name: container
exec:
file: build.sh
args:
# Positional argument
- pos: 1
envKey: IMAGE_TAG
required: true
# Flag arguments
- flag: publish
envKey: PUBLISH
type: bool
default: false
- flag: registry
envKey: REGISTRY
default: "docker.io"
# Saved to a file
- flag: version
outputFile: //version.txtRun with arguments:
flow build container -- v1.2.3 --publish=true --registry=my-registry.comWARNING
Breaking change: Executable arguments now use standard --flag=value syntax with a -- separator. The previous flag=value format (e.g., flow build container v1.2.3 publish=true) is no longer supported. Use -- to separate flow flags from executable arguments, and prefix flag names with --.
Argument types:
pos: Positional argument (by position number, starting from 1)flag: Named flag argument
Command-Line Overrides
Override any environment variable with --param:
flow deploy app --param API_TOKEN=override --param ENVIRONMENT=stagingNOTE
If the outputFile field is used to save a value, it will automatically be cleaned up after the executable finishes running.
Working Directories
Control where executables run with the dir field:
executables:
- verb: build
name: frontend
exec:
cmd: npm run build
dir: "./frontend" # Relative to flowfile
- verb: clean
name: downloads
exec:
cmd: rm -rf downloads/*
dir: "~/Downloads" # User home directory
- verb: deploy
name: from-root
exec:
cmd: kubectl apply -f k8s/
dir: "//" # Workspace root
- verb: test
name: isolated
exec:
cmd: |
echo "Running in temporary directory"
ls -la
dir: "f:tmp" # Temporary directory (auto-cleaned)Directory prefixes:
//: Workspace root directory~/: User home directory./: Current working directoryf:tmp: Temporary directory (auto-cleaned)$VAR: Environment variable expansion
Executable Types
exec - Shell Commands
Run commands or scripts directly:
executables:
- verb: build
name: app
exec:
cmd: npm run build && npm test
- verb: deploy
name: app
exec:
file: deploy.sh
logMode: json # text, logfmt, json, or hiddenOptions:
cmd: Inline command to runfile: Script file to executeinterpreter: Which interpreter runscmd—sh(default) orpython(see below)logMode: How to format command outputcontainer: Run the command or file inside a container image (see below)
Windows
cmd and .sh files run in flow's built-in POSIX shell, so the syntax works everywhere. On Windows it also provides base64, cat, chmod, cp, find, gzip, ls, mkdir, mktemp, mv, rm, shasum, tar, touch, and xargs; any other command must be on PATH. Call a script as bash script.sh rather than ./script.sh, or branch with if: os == "windows". FLOW_CORE_UTILS=true|false overrides the built-ins on any platform.
Running Python
Set exec.interpreter to python to run cmd as a Python script instead of a shell command:
executables:
- verb: run
name: report
exec:
interpreter: python
cmd: |
import json, sys
print(json.dumps({"python": sys.version_info[:2]}))Parameters, arguments, and secrets reach the script through the environment exactly as they do for a shell command, so os.environ is how you read them.
A .py file needs no interpreter at all — the extension implies it:
exec:
file: scripts/analyze.pySetting interpreter explicitly overrides whatever the extension would have implied.
How flow finds Python. A project's virtualenv wins over bare system Python, so a script gets the dependencies its repository installed:
| Order | Source |
|---|---|
| 1 | FLOW_PYTHON_BIN, if set |
| 2 | $VIRTUAL_ENV — an activated virtualenv |
| 3 | <workspace root>/.venv |
| 4 | python3 on the PATH |
| 5 | python on the PATH |
FLOW_PYTHON_BIN is an environment variable rather than a field, so you can pin an interpreter for a whole workspace in its .env file, or for one executable via params. If it is set but does not resolve, the run fails rather than silently falling back to a different interpreter.
On Windows the virtualenv path is Scripts\python.exe, and python is preferred over python3 — python3.exe there is usually the Microsoft Store alias stub rather than a real interpreter.
flow runs cmd from a temporary file rather than python -c, which keeps your code out of the process table and means tracebacks carry real line numbers. It also sets PYTHONUNBUFFERED=1 so output streams as it is produced, and PYTHONDONTWRITEBYTECODE=1 to keep __pycache__ out of your workspace. Set either variable yourself to override.
Running in a container
Set exec.container to run the command inside a container instead of on the host. This gives you a pinned, reproducible toolchain without installing it locally. Requires docker or podman on the PATH.
executables:
- verb: build
name: go-app
exec:
cmd: go build -o bin/app ./cmd/app
container:
image: golang:1.21-alpineThe workspace root is auto-mounted (by default at /workspace) and the executable's directory becomes the working directory inside the container, so relative paths behave the same as they would on the host. Parameters, arguments, and FLOW_* variables are passed in automatically (secrets go through a temporary --env-file, never the command line).
Advanced options:
exec:
cmd: npm test
container:
image: node:18-alpine
runtime: auto # auto (default), docker, or podman
workdir: /app # override the working directory
mountWorkspace: /app # container path for the workspace mount
volumes:
- "//cache:/cache" # //-, ~/-, ./-, or absolute host paths
inheritEnv: true # pass params/args/FLOW_* into the container (default true)
entrypoint: "" # "" uses the image's ENTRYPOINT; unset defaults to sh
user: "1000:1000" # defaults to the host user on Linux
network: hostinterpreter: python works with container too — combine them to get a pinned Python toolchain without installing it locally:
exec:
interpreter: python
cmd: |
import sys
print(sys.version)
container:
image: python:3.13-alpineNotes and limitations:
- By default flow overrides the image entrypoint with
shsocmdbehaves as a shell command on any image. Withinterpreter: pythonthe default entrypoint becomespython3instead. Setentrypoint: ""to use the image's ownENTRYPOINT— with Python that only works if the image'sENTRYPOINTis itself an interpreter. - Host interpreter discovery does not apply inside a container: the image's own
python3is used, andVIRTUAL_ENV,PYTHONPATH,PYTHONHOME, andFLOW_PYTHON_BINare dropped from the container environment because those host paths mean nothing inside it. Install dependencies in the image, or mount them withvolumes. - On Linux, flow runs as your host user by default so mounted files are not root-owned. Set
user: rootto opt out. .bat,.cmd, and.ps1files are not supported withcontainer.containerapplies toexecexecutables only; inlinecmdsteps insideserial/paralleldo not inherit it — reference a container-backed executable instead. (interpreteris available on those steps — see below.)outputFiledestinations for params/args should resolve under the workspace root so the container can see them (use//-prefixed or flow-file-relative paths).- On macOS, Docker Desktop does not share
/var/foldersby default, sodir: f:tmpmay fail to mount; use a workspace-relative directory instead.
Per-step interpreters
Inline cmd steps inside serial and parallel take their own interpreter, so one workflow can mix shell and Python without splitting into separate executables:
executables:
- verb: run
name: pipeline
serial:
execs:
- cmd: ./fetch-data.sh
- cmd: |
import json
print(json.load(open("data.json"))["total"])
interpreter: pythonA step that omits interpreter runs under the shell as before. A step using ref ignores the field — the referenced executable brings its own.
serial - Sequential Execution
Run multiple steps in order:
executables:
- verb: deploy
name: full-stack
serial:
failFast: true # Stop on first failure
execs:
- cmd: docker build -t api .
- cmd: docker build -t web ./frontend
- ref: test api
- cmd: kubectl apply -f k8s/
retries: 3
- cmd: kubectl rollout status deployment/api
reviewRequired: true # Pause for user confirmationThe executable environment variables and executable directory of the parent executable are inherited by the child executables.
Options:
failFast: Stop execution on first failure (default: true)retries: Number of times to retry failed stepsreviewRequired: Pause for user confirmation
Each step also accepts:
name: A label for the step, shown in place of the ref in outputif: An Expr expression gating whether the step runsinterpreter: The interpreter for acmdstep (arefuses its own)args: Arguments to pass to aref'd executable, written as you would type them (--flag=valueor a positional value).$VARexpands against the parent environment. These override what the step would otherwise inherit.
parallel - Concurrent Execution
Run multiple steps simultaneously:
executables:
- verb: test
name: all-suites
parallel:
maxThreads: 4 # Limit concurrent operations
failFast: false # Run all tests even if some fail
execs:
- cmd: npm run test:unit
- cmd: npm run test:integration
- cmd: npm run test:e2e
- ref: lint code
retries: 1The executable environment variables and executable directory of the parent executable are inherited by the child executables.
Options:
maxThreads: Maximum concurrent operations (default: 5)failFast: Stop all operations on first failure (default: true)retries: Number of times to retry failed operations
Each step also accepts:
name: A label for the step, shown in place of the ref in outputif: An Expr expression gating whether the step runsinterpreter: The interpreter for acmdstep (arefuses its own)args: Arguments to pass to aref'd executable, written as you would type them (--flag=valueor a positional value).$VARexpands against the parent environment. These override what the step would otherwise inherit.
launch - Open Applications
Open files, URLs, or applications:
executables:
- verb: open
name: workspace
launch:
uri: "$FLOW_WORKSPACE_PATH"
app: "Visual Studio Code"
- verb: open
name: docs
launch:
uri: "https://flowexec.io"
- verb: open
name: note
launch:
uri: "./note.md"
app: "Obsidian"Options:
uri: File path or URL to open (required)app: Specific application to use
request - HTTP Requests
Make HTTP requests to APIs:
executables:
- verb: deploy
name: webhook
request:
method: POST
url: "https://api.example.com/deploy"
headers:
Authorization: "Bearer $API_TOKEN"
Content-Type: "application/json"
body: |
{
"environment": "$ENVIRONMENT",
"version": "$VERSION"
}
timeout: 30s
validStatusCodes: [200, 201]
logResponse: true
transformResponse: '"Deployed " + fromJSON(body)["status"]'
responseFile:
filename: "deploy-response.json"Options:
method: HTTP method (GET, POST, PUT, PATCH, DELETE)url: Request URL (required)headers: Custom headersbody: Request body — sent as written when it is JSON, otherwise an Expr expressiontimeout: Request timeoutvalidStatusCodes: Acceptable status codeslogResponse: Log response bodytransformResponse: Expr expression to reshape the response before output or file saveresponseFile: Save response to file
Request bodies:
$VAR references in body are expanded first, the same as in url and headers. What happens next depends on the result:
- A JSON object or array is sent as written. This is the common case — write the body as JSON and interpolate values with
$VAR. - Anything else is evaluated as an Expr expression that must produce a string.
The expression form is worth reaching for when a value needs escaping. toJSON quotes and escapes its argument, so a prompt containing quotes or newlines survives intact — where pasting it into a JSON template would produce a malformed body:
body: >
'{"model":' + toJSON(env["MODEL"]) + ',"prompt":' + toJSON(env["PROMPT"]) + '}'Transforming responses with transformResponse:
The transformResponse field is a single Expr expression evaluated after the request completes. Its result replaces the raw response body in any output or responseFile. The expression has access to:
| Variable | Type | Description |
|---|---|---|
body | string | Raw response body |
code | int | HTTP status code (e.g. 200, 404) |
status | string | Full status line (e.g. "200 OK") |
headers | map[string][]string | Response headers |
NOTE
headers is a map[string][]string — each name maps to a slice of values. Access the first value with headers["Content-Type"][0], not headers["Content-Type"].
Common patterns:
# Extract a field from a JSON body
transformResponse: fromJSON(body)["name"]
# Uppercase a status field
transformResponse: upper(fromJSON(body)["status"])
# Format an array as newline-separated output
# NOTE: quote any expression containing '#' - in YAML, a space then '#' starts a comment
transformResponse: 'join(map(fromJSON(body)["items"], #["name"]), "\n")'
# Conditional with fallback
transformResponse: code == 200 ? fromJSON(body)["result"] : "error " + string(code) + ": " + body
# Let binding to avoid reparsing
transformResponse: let data = fromJSON(body); data["id"] + " — " + data["name"]See the Expression Language guide for the full syntax reference.
render - Dynamic Documentation
Process a template file and display its output — useful for status dashboards, reports, and any dynamically-generated text:
executables:
- verb: show
name: status
render:
templateFile: "status-template.md"
templateDataFile: "status-data.json"Options:
templateFile: Markdown template file (required)templateDataFile: JSON/YAML data file for thedatavariabledir: Working directoryparams: Environment variable definitions (available asenv["KEY"])
Available template variables:
| Variable | Type | Description |
|---|---|---|
env | map[string]string | Params and environment variables from the executable |
data | any | Parsed contents of templateDataFile (nil if not set) |
By default a render opens an interactive viewer, and falls back to writing plain text to stdout when it isn't attached to a terminal — in CI, a script, or piped into another command. To force plain text while attached to a terminal, set DISABLE_FLOW_INTERACTIVE=true. See Interactive UI.
data is typed based on the file content — a JSON object becomes a map, a JSON array becomes a slice. Access fields with bracket notation: data["key"] or data[0]["field"].
Template file example — given a status-data.json:
{"service": "api", "version": "2.1.0", "replicas": 3}A status-template.md template:
# Deployment Status
Service: {{ data["service"] }}
Version: {{ data["version"] }}
Replicas: {{ string(data["replicas"]) }}
Environment: {{ env["DEPLOY_ENV"] }}
{{ if data["version"] != "" }}
Last deployed: {{ data["version"] }}
{{ end }}The template syntax uses {{ expression }} delimiters where expressions are evaluated using the Expr language. See the Expression Language guide for syntax and built-ins.
Importing Executables
Generate executables from scripts, Makefiles, package.json scripts, or docker-compose services:
# In flowfile
imports:
- "scripts/deploy.sh"
- "scripts/build.bat"
- "scripts/setup.ps1"
- "Makefile"
- "frontend/package.json"
- "docker-compose.yaml"All imported executables are automatically tagged with generated and their file type (e.g., docker-compose, makefile, package.json).
Script Files
Script files (.sh, .bat, .cmd, .ps1, .py) are imported as single executables with the script's filename as the name and exec as the default verb. Each script type is executed with its native interpreter:
| Extension | Interpreter | Platforms |
|---|---|---|
.sh | Built-in POSIX shell | All (cross-platform) |
.bat, .cmd | cmd.exe /C | Windows |
.ps1 | pwsh or powershell | All (requires PowerShell) |
.py | Resolved Python (see Running Python) | All (requires Python) |
You can use special comments to override executable metadata. The comment syntax depends on the script type:
#!/bin/bash
# f:name=production f:verb=deploy
# f:description="Deploy to production environment"
# f:tag=production f:tag=critical
# f:timeout=10m
echo "Deploying to production..."
kubectl apply -f k8s/@echo off
REM f:name=production f:verb=deploy
REM f:description="Deploy to production environment"
REM f:tag=production f:tag=critical
REM f:timeout=10m
echo Deploying to production...
kubectl apply -f k8s\# f:name=production f:verb=deploy
# f:description="Deploy to production environment"
# f:tag=production f:tag=critical
# f:timeout=10m
Write-Host "Deploying to production..."
kubectl apply -f k8s/#!/usr/bin/env python3
# f:name=production f:verb=deploy
# f:description="Deploy to production environment"
# f:tag=production f:tag=critical
# f:timeout=10m
import subprocess
print("Deploying to production...")
subprocess.run(["kubectl", "apply", "-f", "k8s/"], check=True)See the generated configuration reference for more details.
Makefiles
Makefile targets are imported as executables with a verb and name that best represents the target.
# Makefile
# f:name=app f:verb=build f:description="Build the application"
build:
go build -o bin/app ./cmd/app
# Run all tests
test:
go test ./...
# f:visibility=internal
clean:
rm -rf bin/See the generated configuration reference for more details on overriding executable configuration.
Package.json Scripts
NPM scripts from package.json are imported as executables with a verb and name that best represents the script name.
{
"scripts": {
"build": "webpack --mode production",
"test": "jest",
"dev": "webpack-dev-server --mode development",
"lint": "eslint src/"
}
}This creates executables like:
build- Runs the build scripttest- Runs the test scriptstart dev- Runs the development serverlint- Runs the linter
Docker Compose Services
Docker Compose files are imported to create executables for managing services:
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
db:
image: postgres:13
environment:
POSTGRES_DB: myapp
redis:
image: redis:6This creates executables like:
start app- Start the app servicestart db- Start the database servicestart redis- Start the Redis servicestart(alias: all, services) - Start all servicesstop(alias: all, services) - Stop all servicesbuild app- Build the app service (if build config exists)
Executable References
Reference other executables to build modular workflows:
executables:
# Reusable components
- verb: build
name: api
exec:
cmd: docker build -t api .
- verb: test
name: api
exec:
cmd: npm test
# Composite workflows
- verb: deploy
name: full
serial:
execs:
- ref: build api
- ref: test api
- cmd: kubectl apply -f api.yaml
# Cross-workspace references (requires public visibility)
- verb: deploy
name: with-monitoring
serial:
execs:
- ref: deploy full
- ref: trigger monitoring/slack:deployment-completeReference formats:
ref: build api- Current workspace/namespaceref: build workspace/namespace:api- Full referenceref: build workspace/api- Specific workspaceref: build namespace:api- Specific namespaceref: build workspace/parent/child:api- Nested namespaceref: build ./parent/child:api- Nested namespace in the parent's workspace
Cross-workspace requirements:
- Referenced executables must have
visibility: public - Private, internal, and hidden executables cannot be cross-referenced
What's Next?
Now that you understand all executable types and options:
- Build complex workflows → Advanced workflows
- Secure your automation → Working with secrets
- Generate project templates → Templates & code generation

