Skip to content

CLI Reference

Global Options

# Show version
oduflow --version

# Disable anonymous usage telemetry for this run only (e.g. test or CI runs);
# place it before any subcommand
oduflow --no-telemetry --transport http
oduflow --no-telemetry stack apply oduflow.yaml

--no-telemetry overrides disable_telemetry in oduflow.toml for the current process only. See Telemetry.

# Log only warnings and errors (default: info); also before any subcommand
oduflow --log-level warning stack apply oduflow.yaml

--log-level takes debug, info, warning or error and applies to the server too, its HTTP access log included. warning leaves a one-shot command such as stack apply printing just its result; at info the progress messages of long steps (image pulls, environment creation) are shown as well. A command that fails on invalid input prints one error line; debug adds the traceback.

Running the Server

# Single-user / stdio mode (default — for local MCP clients)
oduflow
uvx oduflow

# Server / HTTP mode (for remote and multi-user deployments)
oduflow --transport http
oduflow -t http
uvx oduflow --transport http
uvx oduflow -t http

Shared infrastructure (Docker network, PostgreSQL, team directories) is initialized automatically on startup.

stdio mode — the server communicates over stdin/stdout. The MCP client starts the process directly; no network port is needed. Ideal for local clients like Claude Desktop, Windsurf, etc.

HTTP mode — starts a persistent HTTP server on http://0.0.0.0:8000 by default. Exposes the MCP endpoint at /mcp, a Web Dashboard at /, and a REST API at /api/. MCP uses Bearer tokens; the dashboard and its REST/WS API use session cookies obtained through the login form (with TOTP when enabled). HTTP Basic is not accepted.

Configuration is loaded from oduflow.toml (see Installation). See Quick Start for MCP client configuration examples for both modes.

To reconcile a declarative Stack before starting the server:

oduflow --stack /path/to/oduflow.yaml --stack-team 1 --transport http

Startup stops with a non-zero exit if Stack validation, preflight, or apply fails. See Declarative Stacks.

Dashboard Two-Factor Authentication

Run locally on the server as the Oduflow service user, with the server's existing configuration and persistent data directory. Docker users should execute these commands inside the running Oduflow container with an interactive terminal.

# Print a local QR code and enable TOTP after confirming an authenticator code
oduflow ui-2fa setup --team 1

# Confirm disabling TOTP and revoke full operator UI sessions
oduflow ui-2fa reset --team 1

--team defaults to 1. Setup requires a configured ui_password; an existing factor must be reset before replacement. Reset returns the UI to password-only login until setup runs again. Commands use the same ODUFLOW_TOML configuration lookup as the server, do not require Docker access, and do not start the server. Changes take effect without a restart. Shared links and MCP clients are unaffected. See UI 2FA for enrollment, session expiry, and recovery details.

License Commands

Manage the commercial license on the server. This is the only license interface under a Custom license, where the dashboard shows no license dialog to your clients.

# Holder, validity, and for a Custom license the brand, domains and white-label state
oduflow license status

# Verify and install a key file into the config directory
oduflow license install ./license.key

# Install a renewed key from the license server, if one was paid
oduflow license refresh

# Print a payment link when the license period ended without a subscription
oduflow license subscribe

The commands read the existing configuration only and do not require Docker. Restart the service after installing or renewing a license so MCP clients see the change; the dashboard picks it up within a minute.

Server Settings Console

The Server settings console at /admin edits oduflow.toml from the browser. Its password is the one setting it cannot create for itself:

# Generate a console password, store it as [admin] password, print it once
oduflow admin enable

# Replace the password (signs every console session out)
oduflow admin enable --reset

# Remove the password; /admin answers 404 again
oduflow admin disable

# Optional: authenticator-app 2FA for the console
oduflow ui-2fa setup --admin
oduflow ui-2fa reset --admin

enable and disable write through the same validated, history-keeping writer as the console. Restart Oduflow afterwards so the running server picks the password up.

Declarative Stack Commands

# Local syntax and schema validation (does not require Docker)
oduflow stack validate oduflow.yaml

# Read-only comparison with live resources
oduflow stack plan oduflow.yaml --team 1

# Reconcile under the team's lock
oduflow stack apply oduflow.yaml --team 1

# JSON status: drift plan plus the last successful apply record
oduflow stack status oduflow.yaml --team 1

Stack apply is additive and non-destructive in V1. Existing resources owned by someone else and environment changes that require replacement are reported as conflicts; no automatic deletion or pruning is performed.

System Commands

# Destroy all shared infrastructure (requires no active environments)
oduflow destroy

# Three-way merge deployed files with the installed bundled versions
oduflow upgrade

# Skip the confirmation prompt and overwrite conflicts with the new bundle
oduflow upgrade --force

# Preview the unified host resource plan and managed config diffs
oduflow retune-postgres

# Back up and write configs; stage production Odoo configs in containers
oduflow retune-postgres --apply

# Upgrade the Oduflow package itself, reconcile bundled files, restart
oduflow self-update

# Non-interactive: overwrite bundle conflicts; or skip the service restart
oduflow self-update --force
oduflow self-update --no-restart

Cluster Disaster Recovery

Cluster-wide WAL control and whole-cluster PITR are operator actions: they affect every team's productions at once, so they are not MCP tools. They are available in the server settings console (/admin → Cluster DR) and on the owner CLI:

# Cached cluster WAL state (queue, disk reserve, protection latch)
oduflow wal status

# Archiving control / disk-protection recovery (see Production Hosting docs)
oduflow wal pause   --confirm ALL-PRODUCTIONS
oduflow wal resume  --confirm ALL-PRODUCTIONS
oduflow wal retry   --confirm ALL-PRODUCTIONS
oduflow wal recover --confirm ALL-PRODUCTIONS
oduflow wal release --confirm ALL-PRODUCTIONS

# DISASTER RECOVERY: restore the whole production PostgreSQL cluster from
# WAL-G (optionally to a point in time). Also the "resurrect production
# elsewhere" path: a fresh install with the same [backup] section rebuilds
# the cluster from S3.
oduflow restore-cluster --confirm RESTORE-CLUSTER
oduflow restore-cluster --target-time "2026-07-10 12:00:00+00" --confirm RESTORE-CLUSTER

restore-cluster stops every production Odoo container, restores the cluster from the newest suitable base backup plus WAL replay, and restarts the productions it stopped. The displaced data directory is kept inside the Docker volume for manual cleanup. See Production Hosting for the full semantics.

retune-postgres accounts for [production].enabled and does not restart containers. For existing productions, --apply also regenerates odoo.conf with the planned worker count and copies it into the container; the command then lists every PostgreSQL and Odoo container that should be restarted. It refuses to replace a custom PostgreSQL config unless --apply --force is given. See PostgreSQL resource planning.

oduflow upgrade reconciles each team's odoo.conf, agent guides, and bundled sanitize script against a stored pristine baseline. It compares complete file contents, updates an untouched file, preserves local-only changes, and uses git merge-file when both the local and bundled versions changed. Before a live update, the previous file is saved under <team-data>/.bundled_upgrade/backups/.

On a clean merge the live file and baseline advance together. On conflict the live file and accepted baseline stay untouched; the merge result is written to *.oduflow-merge. Existing customized installations with no baseline receive *.oduflow-new for a one-time manual reconciliation. Resolve/install the sidecar and remove it; until then the command exits with status 1.

--force makes the command fully non-interactive: it skips the confirmation prompt and resolves conflicts, legacy files, and merge failures in favour of the new bundle. The replaced live file is saved under <team-data>/.bundled_upgrade/backups/, the baseline advances, and any stale sidecar is removed, so a forced run leaves nothing to review and exits 0. Clean merges still merge, local-only changes are still left untouched, and a first-line # KEEP remains an unconditional opt-out.

This command is separate from upgrading the Python package (for example, uv tool upgrade oduflow). It does not manage postgresql.conf; use oduflow retune-postgres for PostgreSQL planning and updates.

Before reconciling, oduflow upgrade checks that the next server start accepts the configured [database].image and can complete a PostgreSQL upgrade it requests (see Upgrading PostgreSQL). If environments or unexpected databases block the upgrade, or the image needs attention, the command lists the problems and exits with an error without changing anything, and self-update does not restart the service.

oduflow self-update chains the whole documented upgrade: it compares the installed version with the latest GitHub release, upgrades the package through its own installer (uv tool upgrade oduflow for a uv tool install, otherwise pip install --upgrade oduflow in the same environment), verifies the installed version, runs the bundled-file reconciliation above through a fresh process in that Python environment, and restarts the systemd service when the unit installed by oduflow systemd-install exists and the command runs as root. --force is forwarded to the reconciliation, and it also finishes an interrupted upgrade: if the package is already at the latest version — for example after a first run stopped on a bundle conflict — the command reconciles and restarts instead of reporting "already up to date" and doing nothing. --no-restart leaves the running server on the old version until you restart it yourself.

If the installer succeeds but the advertised release is not installed (for example, uv has a version pin or the package index has not received the release yet), the command exits with an error before reconciliation or restart. Check the installer's constraints and index, then retry. A uv tool upgrade also checks that uv's tool directory contains the running installation; use the installing user and original UV_TOOL_DIR if they differ.

It refuses, with an error, installations it cannot upgrade durably: a container (a package upgraded inside the oduist/oduflow container reverts when the container is recreated — pull the new image and recreate it instead, see Docker), a source checkout or editable install (update those with git pull), an ephemeral uvx run (use uvx oduflow@latest to refresh the cached version), and an environment pip cannot upgrade in place — a virtualenv created without pip, or a site-packages the current user cannot write, where pip install --upgrade would install a second copy into ~/.local that the running service never loads. Re-run those as the user that owns the installation.

Template Commands

All template commands accept --team to specify the team ID (default: 1).

# Generate a clean template from a Docker image
oduflow init-template --odoo-image odoo:19.0 --template-name myproject [--modules base,web,sale] [--force] [--team 1]

# Save a branch environment as the new template.
# Other environments on this template keep their filestore changes by default;
# pass --reset-env-changes to discard them and reset to the new baseline.
oduflow template-from-env <branch> --template-name myproject [--reset-env-changes] [--team 1]

# Re-apply a template's current filestore to live overlay environments
# (non-destructive by default; --reset-env-changes discards env deltas)
oduflow refresh-template <template_name> [--reset-env-changes] [--team 1]

# Attach or replace a template filestore from a local dir, archive, rsync://, or SSH rsync source
oduflow attach-filestore <template_name> <source> [--strip-prefix auto|none|PREFIX] [--reset-env-changes] [--team 1]

# List all template profiles
oduflow list-templates [--team 1]

# Delete a template profile
oduflow delete-template <template_name> [--team 1]

# Import a template from a running Odoo instance
oduflow import-template <odoo_url> <master_pwd> --template-name myproject [--db-name <db>] [--without-filestore] [--team 1]

# Import (or incrementally re-sync with --overwrite) from an S3 prefix or a
# local path holding dump.* + filestore/; a single dump file imports DB-only
oduflow import-template s3://bucket/backups/mydb/ --template-name myproject [--overwrite] [--team 1]
oduflow import-template /backups/mydb/ --template-name myproject [--overwrite] [--team 1]

# A single Odoo backup .zip already sitting in a bucket
oduflow import-template s3://bucket/backups/mydb-2026-10-13.zip --template-name myproject [--team 1]

# Reload the template DB from files already placed in the template directory
# (an external rsync/scp drop) and refresh its metadata
oduflow import-template --template-name myproject --refresh [--team 1]

# Run a saved, enabled SSH sync schedule (cron normally invokes this)
oduflow sync-template myproject [--team 1]
# Only consume a manual request queued by the dashboard
oduflow sync-template myproject --pending [--team 1]

For a private bucket, pass --s3-access-key with --s3-secret-key (plus --s3-endpoint/--s3-region where needed). The secret key is stored as the team secret s3-template-<template> and the template records only a secret:<name> reference, so a later --overwrite re-sync needs no key — omit --s3-secret-key and the stored one is reused. It is required again when the endpoint or access key changes. --s3-secret-key defaults to $ODUFLOW_S3_SECRET_KEY, which keeps the key out of your shell history; pass secret:<name> to sign with an existing team secret instead.

template-from-env, refresh-template, attach-filestore, and import-template with --overwrite/--refresh are non-destructive for live overlay environments: each is unmounted and remounted against the new template filestore while keeping its upper changes. Use --reset-env-changes (on template-from-env/refresh-template/attach-filestore) to reset environments to the clean baseline instead. Without --overwrite, import-template creates a new template and refuses an existing template name.

Scheduled SSH sync uses retained filestore generations and opt-in environment DB/filestore resets. Ordinary template refresh leaves pinned environments on their previous generation and lists them in its output. See Scheduled SSH sync for source preparation, cron installation and recovery.

Service Commands

# List all managed services
oduflow list-services [--team 1]

# List persistent PostgreSQL databases for auxiliary services (no passwords)
oduflow list-service-databases [--team 1]

Create, inspect, rotate, and delete databases through the matching MCP tools with oduflow call, for example:

oduflow call create_service_database '{"name":"events"}'
# Or on the dedicated production PostgreSQL cluster (requires production hosting)
oduflow call create_service_database '{"name":"events","cluster":"prod"}'
oduflow call get_service_database '{"name":"events"}'
oduflow call rotate_service_database_password '{"name":"events"}'
oduflow call delete_service_database '{"name":"events"}'

Maintenance Commands

# Show orphaned databases, workspaces, and port entries (dry-run by default)
oduflow cleanup [--team 1]

# Same as above — only show what would be removed
oduflow cleanup --dry-run [--team 1]

# Actually remove orphaned resources
oduflow cleanup --force [--team 1]

The cleanup command detects and removes resources that no longer have a corresponding running or stopped container:

  • Orphan databases — PostgreSQL databases with the oduflow_ prefix that have no matching environment container
  • Orphan workspaces — workspace directories on disk that have no matching environment container
  • Orphan port entries — entries in ports.json that have no matching environment container

By default, cleanup runs in dry-run mode and only reports what would be removed. Use --force to actually delete the orphaned resources; anything that could not be removed is reported on an Error: line. The dashboard's Cleanup button shows the same preview and can also remove unused Docker images.

Systemd Service

# Install and enable systemd service
oduflow systemd-install

# Remove the systemd service
oduflow systemd-uninstall

The systemd-install command generates a unit file at /etc/systemd/system/oduflow.service, runs daemon-reload, and enables the service.

See Auto-start with systemd for the full setup guide.

Tool Introspection

# List all registered MCP tools with parameters
oduflow list [--verbose]

Direct Tool Invocation

You can invoke any registered MCP tool directly from the terminal using oduflow call, without running the server or connecting an MCP client. This is useful for scripting, debugging, and manual operations.

# List all available tools with their parameters
oduflow call

# Call a tool with positional arguments (mapped to parameters in order)
oduflow call create_environment dev "" "" https://github.com/owner/repo.git odoo:19.0
oduflow call delete_environment dev
oduflow call list_environments
oduflow call get_environment_logs main 50
oduflow call run_odoo_command dev "ls /mnt/extra-addons"
oduflow call create_service redis redis:7 6379

# Call a tool with JSON-encoded arguments
oduflow call create_environment '{"branch":"dev","repo_url":"https://github.com/owner/repo.git","odoo_image":"odoo:19.0","template_name":"myproject"}'

# Service with NET_ADMIN capability (VPN / tun / iptables)
oduflow call create_service '{"name":"vpn","image":"linuxserver/wireguard","port":51820,"net_admin":true}'

# Type coercion is automatic: int, bool, and float parameters are cast from strings
oduflow call get_environment_logs dev 500

Remote Tool Invocation

oduflow client calls the same registered tools on a running Oduflow HTTP server through FastMCP. Unlike oduflow call, it does not load the local oduflow.toml, access local Docker, or execute server functions in the client process.

Configure the exact full or scoped MCP endpoint and its Bearer credential:

export ODUFLOW_MCP_URL="https://oduflow.example.com/mcp"
export ODUFLOW_MCP_TOKEN="<team-auth-token>"

# Tools advertised by this endpoint
oduflow client list
oduflow client list --verbose

# Read live help generated from a tool's input schema
oduflow client create_environment --help

# Call team-wide tools
oduflow client list_environments
oduflow client create_environment \
  --repo-url https://github.com/owner/addons.git \
  --odoo-image odoo:19.0 \
  --template-name myproject

The client reads the live tools/list response, converts kebab-case flags to MCP parameter names, validates basic scalar/JSON types, and then calls the tool. A single JSON object is also accepted for complex arguments:

oduflow client create_service '{
  "name": "redis",
  "image": "redis:7",
  "port": 6379
}'

Client options must appear before the tool name:

oduflow client --timeout 1200 --json list_environments
oduflow client --env demo pull_and_apply --upgrade sale_custom --strict

When a remote schema requires env_name, the client uses --env, then ODUFLOW_ENV_NAME, then the current Git branch. When create_environment requires branch, it uses the current Git branch unless --branch is supplied; other tools that take a required branch, such as create_production, always need it spelled out. An explicit environment override is useful when an environment name differs from its source branch.

For confined development access, use the environment's scoped endpoint and Secret Key from More → MCP Access:

export ODUFLOW_MCP_URL="https://oduflow.example.com/mcp/feature-x"
export ODUFLOW_MCP_TOKEN="<environment-secret-key>"

# env_name is absent from the scoped schema and injected by the server
oduflow client get_environment_info
oduflow client pull_and_apply --upgrade sale_custom --strict
oduflow client run_odoo_tests --modules sale_custom

The scoped server advertises only its allowlisted single-environment tools, so team-wide commands such as list_environments and create_environment are not available. The client does not maintain a second authorization list; the live server schema remains authoritative.

For scripts and CI, --json emits the complete MCP result and tool failures return a non-zero exit code. To avoid storing a token in the environment, pass it on standard input:

printf '%s\n' "$TOKEN_FROM_SECRET_STORE" | \
  oduflow client --token-stdin --json list_environments

In summary, oduflow call <tool> is local in-process execution, while oduflow client <tool> is remote authenticated MCP execution.