Skip to content

Setting up a development environment

To add features to the repository follow the following procedure to setup a working development environment.

Installation

Install the project and its dependencies to develop on the code.

Step 1 - install uv

Follow the installing uv instructions.

Step 2 - install project and dependencies

uv sync --all-groups --all-extras

This creates a virtual environment at .venv with the latest dependencies and version of python. This command can be repeated to update the venv i.e. when switching to a branch with different dependencies, or after not having worked on the project for a while.

You can either activate the venv with source .venv/bin/activate or prefix commands with uv run <command>. We’ll use the latter form throughout this documentation.

More details in About UV.

Running tests

The test suite is split across four pytest invocations. See the testing guide for the full picture; the short version:

Layer Command Needs Postgres / Redis?
Unit tests + doctests uv run pytest No
Integration tests uv run pytest test/integration_tests Yes (Postgres + Redis)
Celery acceptance tests uv run pytest test/acceptance_tests/celery Yes (Postgres + Redis)
LLM/embedding acceptance tests uv run pytest test/acceptance_tests/search/llm Yes (Postgres + Redis + LLM keys)

The Celery and LLM suites run as their own GitHub Actions workflows (run-celery-acceptance-tests.yml, run-llm-acceptance-tests.yml, run-cli-acceptance-tests.yml). The integration command above is what run-tests.yml runs.

Run unit tests (no services required)

uv run pytest

This runs every test under test/unit_tests/ plus the doctests in orchestrator/core. No Postgres, no Redis, no Docker — fast feedback for day-to-day development.

Run integration tests

Integration tests need Postgres and Redis. Pick whichever provisioning style fits your workflow.

Pick the right CACHE_URI for your Redis setup:

Setup CACHE_URI
Manually started Redis with no AUTH (e.g. brew services start redis, GitHub Actions service container) redis://localhost:6379/0
Bundled docker/pytest-support/docker-compose.yaml (Redis with password nwa) redis://:nwa@localhost:6379/0
Ephemeral testcontainers (auto-set; you don’t write this URI) generated

The Postgres URI in all the examples below assumes you started Postgres with the conventional nwa / nwa credentials. Adjust if yours differ.

Option A — Let testcontainers start ephemeral services (easiest):

If DATABASE_URI and CACHE_URI are both unset, the test runner starts a disposable Postgres and Redis container for the test session via testcontainers and removes them on exit. Requires Docker to be running.

unset DATABASE_URI CACHE_URI
uv run pytest test/integration_tests

Option B — Bring your own services (env vars, passwordless Redis):

export DATABASE_URI=postgresql+psycopg://nwa:nwa@localhost:5432/orchestrator-core-test
export CACHE_URI=redis://localhost:6379/0
uv run pytest test/integration_tests

This matches CI. It is also what example-orchestrator and other downstream consumers rely on, so the env-var contract is supported.

Option C — Use the bundled docker-compose stack (Redis password nwa):

docker compose -f docker/pytest-support/docker-compose.yaml up -d
export DATABASE_URI=postgresql+psycopg://nwa:nwa@localhost:5432/orchestrator-core-test
export CACHE_URI=redis://:nwa@localhost:6379/0   # note the ':nwa@' password
uv run pytest test/integration_tests

If Docker is unreachable and no env vars are set, the runner aborts with a clear message pointing at the three options above.

Run the Celery acceptance tests

The Celery suite uses the same CACHE_URI rules — the snippet below assumes passwordless Redis. For the docker-compose stack use CACHE_URI=redis://:nwa@localhost:6379/0 instead.

export DATABASE_URI=postgresql+psycopg://nwa:nwa@localhost:5432/orchestrator-core-test
export CACHE_URI=redis://localhost:6379/0
uv run pytest test/acceptance_tests/celery

Run the LLM/embedding acceptance tests

Same CACHE_URI rules as above. The snippet assumes passwordless Redis.

export DATABASE_URI=postgresql+psycopg://nwa:nwa@localhost:5432/orchestrator-core-test
export CACHE_URI=redis://localhost:6379/0
# plus the LLM credentials documented in test/acceptance_tests/search/llm/README.md
uv run pytest test/acceptance_tests/search/llm

If you do not encounter any failures, you should be able to develop features in the orchestrator-core.

Adding to the documentation

Documentation for the Orchestrator is written by using Mkdocs. To contribute to them follow the instructions above to step 2, you can then develop them locally by running:

uv run mkdocs serve

This should make the docs available on your local machine here: http://127.0.0.1:8000/orchestrator-core/

Changing the Core database schema

When you would like to change the core database schema, first ensure you have the following:

  • An up-to-date development environment
  • A postgres instance and orchestrator-core-test database setup as described in Running tests
  • Venv activated with source .venv/bin/activate (or prefix alembic commands with uv run)

Then execute the following steps to create and test your schema change.

# Point alembic to the right config file
export ALEMBIC_CONFIG=orchestrator/core/migrations/alembic.ini

# Run all current migrations
alembic upgrade heads

# add or change models in orchestrator/core/db/models.py
vim orchestrator/core/db/models.py

# Generate a migration for your change
alembic revision --autogenerate -m "Demonstrating schema changes"

# Output of the previous command mentions the generated migration file:
#   Generating /.../2025-06-27_68d7ec0a554c_demonstrating_schema_changes.py ...  done
# Please open and review it carefully.

# Show the revision history and the current version of your database
alembic history -i
# 161918133bec -> 68d7ec0a554c (schema) (head), Demonstrating schema changes.
# 68d14db1b8da -> 161918133bec (schema) (current), Add is_task to workflow.
# fc5c993a4b4a -> 68d14db1b8da (schema), Make workflow description mandatory.

# Test the upgrade path of your migration
alembic upgrade 68d7ec0a554c

# Test the downgrade path of your migration
alembic downgrade 161918133bec

About UV

uv is a very fast replacement for pip and flit which also adds a lot of functionality. This section explains a few concepts relevant to know when developing on the orchestrator-core. For a full overview consult the uv documentation.

Adding and removing dependencies

Instead of manually editing pyproject.toml you can use uv add.

For example, let’s say we want to become an enterprise application :-)

uv add django  # adds "django>=5.2.3" to pyproject.toml

Apart from updating pyproject.toml uv also installs the dependency (and subdependencies) in your environment.

It is a uv default to specify the lower limit. You can also let uv set an upper limit or set it explicitly:

uv add django --bounds minor   # "django>=5.2.3,<5.3.0"
uv add django --bounds major   # "django>=5.2.3,<6.0.0"
uv add 'django>=5.2.3,<6.0.0'  # "django>=5.2.3,<6.0.0"

To remove the dependency you can run:

uv remove django

Note: The add/remove commands can be given the options, --group <groupname> or --optional <optionalname> to target a specific group or optional.

Python interpreter

uv will create the venv with the latest python version supported by orchestrator-core. If that version is not found on your machine, it is automatically downloaded. Check the output of uv help python how to change this behavior.

uv sync

The uv sync syncs your environment (.venv) based on the options you give it and what’s configured in pyproject.toml.

A few examples of different options:

# base dependencies
uv sync --no-dev

# base + development dependencies
uv sync

# base + development + celery dependencies
uv sync --extra celery

# base + development + mkdocs dependencies
uv sync --group docs

uv removes anything not specified in the command or pyproject.toml to ensure a clean and reproducible environment.

Upgrade dependencies

To upgrade all dependencies in the lockfile to the latest version (taking version constraints from pyproject.toml into account), run the sync command with --upgrade:

uv sync --all-groups --all-extras --upgrade

The github project has Renovate enabled which can create PRs for dependency upgrades.

What is uv.lock for?

The lockfile is used for development and testing of the orchestrator-core. It captures the exact set of dependencies used to create the local environment.

Anytime you execute uv run or uv sync the lockfile is updated if necessary. These updates must be committed because the CI pipeline will fail otherwise. It can also be updated explicitly with uv lock.

When installing orchestrator-core in another project the lockfile is no longer relevant. In that case only the broad requirements from pyproject.toml apply.

More information on the uv website.

Useful settings

SQLAlchemy logging

WFO uses SQLAlchemy for its ORM and DB connection management capabilities.

To get information about which DB queries it is performing, adjust it’s loglevel through this environment variable:

LOG_LEVEL_SQLALCHEMY_ENGINE=INFO

Set it to DEBUG for even more information.

Both INFO and DEBUG generate a lot of logging! It is not recommended to use this in production.

GraphQL query statistics

To get basic statistics per executed GraphQL Query, set the following environment variable:

ENABLE_GRAPHQL_STATS_EXTENSION=true

This will add the following details to the result of the query, as well as logging them.

{
    "data": [],
    "extensions": {
        "stats": {
            "db_queries": 7,
            "db_time": 0.006837368011474609,
            "operation_time": 0.11549711227416992
        }
    }
}

The following should be noted: * this requires monitoring of executed SQLAlchemy cursor events, which may have some overhead * the number of queries can most likely be skewed by async FastAPI REST calls that take place, so it’s recommended to use this in an isolated and controlled environment

GraphQL query profiling

It is possible to profile GraphQL queries by enabling the PyInstrument extension:

ENABLE_GRAPHQL_PROFILING_EXTENSION=true

This will create a file pyinstrument.html in the repository root which shows which parts of the code are taking up most of the execution time.

Note that you need to have the orchestrator-core’s test dependencies installed.

This has a lot of overhead and we advise you to not use this in production.