Skip to main content

Describe Celery workflows in YAML, JSON, or a Python dict, with optional per-step conditions.

Project description

CeleryFlow

PyPI Python License: MIT CI

For Celery users who outgrew chain() and group() but don't want to move to Temporal or Prefect.

CeleryFlow lets you describe a multi-step Celery workflow as a YAML (or JSON, or Python dict) config, with optional per-step conditions based on the payload. The flows you define become regular Celery tasks, so your existing broker, workers, monitoring, and retry behavior keep working unchanged.

If you're starting from scratch and don't already have Celery in production, you probably want one of the dedicated workflow engines (Temporal, Prefect, Dagster, Hatchet) over this. They each have their own worker model, persistence layer, and UI. CeleryFlow exists for the case where you already have Celery, have already invested in it, and just want a cleaner way to compose tasks.

I built a version of this internally for an e-commerce platform and ran it in production for several years. This is the open-source rewrite with the business-specific bits stripped out.

What you get

  • Define flows in YAML, JSON, or a Python dict.
  • Reuse named sub-flows inside larger flows.
  • Skip steps with conditions like {"sn": {"$eq": "1234"}} using Mongo-style operators ($eq, $ne, $in, $gte, etc.). You can add your own.
  • Optional structured logging on start / success / failure / retry.
  • The flows are still regular Celery tasks. Retries, routing, Flower, and any monitoring you already have keep working.

What you don't get

CeleryFlow is intentionally small. It doesn't add:

  • A separate server / orchestrator process (everything runs in your existing Celery workers).
  • A web UI (Flower or your existing Celery dashboard still applies).
  • Durable execution / workflow replay (if your worker dies mid-flow, you're at Celery's mercy, not CeleryFlow's).
  • Cross-language workflows (Python only, like Celery itself).

If you need any of the above, you probably want Temporal or Prefect instead.

Install

pip install celeryflow              # core
pip install "celeryflow[yaml]"      # if you want YAML configs
pip install "celeryflow[redis]"     # if you want Redis as the broker

Python 3.11 or newer. During development I use uv, but you don't need it to install the package.

Quickstart

from celeryflow import CeleryFlow, EventTask, FlowBuilder

app = CeleryFlow("my_worker")
app.conf.update(broker_url="redis://localhost:6379/0",
                result_backend="redis://localhost:6379/0")

# 1. Write some tasks.
@app.task(name="order.validate", base=EventTask)
def validate(payload):
    assert "sn" in payload
    return payload

@app.task(name="order.charge", base=EventTask)
def charge(payload):
    payload["charged"] = True
    return payload

@app.task(name="order.notify", base=EventTask)
def notify(payload):
    payload["notified"] = True
    return payload
  1. Describe the flow. CeleryFlow accepts the definition in three formats — a Python dict, a YAML file, or a JSON file. Pick whichever fits your project.

Option A — Python dict, inline:

FlowBuilder.from_dict(app, {
    "work-flows": [
        {"name": "checkout", "tasks": ["order.validate", "order.charge"]},
    ],
    "main-flows": [
        {
            "name": "PlaceOrder",
            "flows": [
                {"flow": "checkout"},
                {"task": "order.notify"},
            ],
        },
    ],
})

Option B — YAML file:

# flows.yaml
work-flows:
  - name: checkout
    tasks:
      - order.validate
      - order.charge

main-flows:
  - name: PlaceOrder
    flows:
      - flow: checkout
      - task: order.notify
FlowBuilder.from_yaml(app, "flows.yaml")
# Globs work too — multiple files get merged:
FlowBuilder.from_yaml(app, "config/flows/*.yaml")

Option C — JSON file:

{
  "work-flows": [
    {"name": "checkout", "tasks": ["order.validate", "order.charge"]}
  ],
  "main-flows": [
    {
      "name": "PlaceOrder",
      "flows": [
        {"flow": "checkout"},
        {"task": "order.notify"}
      ]
    }
  ]
}
FlowBuilder.from_json(app, "flows.json")

All three produce the same PlaceOrder task. Then run it:

# 3. Run it.
result = app.execute_flow("PlaceOrder", {"sn": "1234"})
print(result.get())
# -> {"sn": "1234", "charged": True, "notified": True}

Conditional steps

FlowBuilder.from_dict(app, {
    "main-flows": [
        {
            "name": "RefundFlow",
            "flows": [
                {"task": "order.validate"},
                # Only charge when it's a new subscription worth >= 100.
                {"task": "order.charge",
                 "condition": {"buy_action": {"$eq": "NEW_SUBSCRIPTION"},
                               "amount":     {"$gte": 100}}},
            ],
        },
    ],
})

When a condition doesn't match, apply_async raises celeryflow.exceptions.ConditionFailed. Catch it in a link_error or custom callback if you'd rather skip the step quietly instead of failing the chain.

Adding your own operator

from celeryflow.conditions import register_operator

register_operator("$startswith",
                  lambda actual, expected: isinstance(actual, str)
                                            and actual.startswith(expected))

Logging

import logging
from celeryflow import EventTask

EventTask.bind_logger(logging.getLogger("orders"))

Each task call will log Start, End, Retry, or Failed. The log records carry task_id, process_id, parent_id, task_name, and a correlation_id if the payload has one.

Development

git clone https://github.com/ChenYuTingJerry/CeleryFlow.git
cd CeleryFlow
uv sync --all-extras

# Unit tests (no broker needed):
uv run pytest -m "not integration"

# Integration tests (need Redis):
docker compose up -d
uv run pytest -m integration
docker compose down

# Lint:
uv run ruff check src tests

Project layout

celeryflow/
├── src/celeryflow/
│   ├── __init__.py        public API
│   ├── app.py             CeleryFlow Celery subclass
│   ├── builder.py         FlowBuilder — parses and registers flows
│   ├── tasks.py           FlowTask / EventTask base classes
│   ├── conditions.py      condition operators
│   ├── encoding.py        JSON encoder (Decimal / Enum / datetime)
│   ├── utils.py           StringConvert / DictConvert
│   └── exceptions.py
├── tests/
├── docker-compose.yml     Redis for integration tests
└── pyproject.toml

If you already use Celery

CeleryFlow doesn't change anything you already have. Your @app.task definitions, broker, workers, and monitoring all keep working. The three things CeleryFlow adds are:

  • CeleryFlow(...) — a Celery subclass. Use it instead of Celery. Takes a worker_name and otherwise behaves the same.
  • EventTask — a Task subclass. Pass base=EventTask to tasks you want in a flow. (You can also use it on every task if you just want the logging.)
  • FlowBuilder.from_* — registers an entry task per flow. When that entry task runs, it splices the rest of the flow into the chain.

So a minimal switch looks like:

# before
from celery import Celery
app = Celery("my_worker", broker="redis://...")

@app.task
def step_a(payload): ...

@app.task
def step_b(payload): ...
# after
from celeryflow import CeleryFlow, EventTask, FlowBuilder
app = CeleryFlow("my_worker", broker="redis://...")

@app.task(base=EventTask)
def step_a(payload): ...

@app.task(base=EventTask)
def step_b(payload): ...

FlowBuilder.from_dict(app, {
    "main-flows": [
        {"name": "MyFlow",
         "flows": [{"task": "step_a"}, {"task": "step_b"}]},
    ],
})

app.execute_flow("MyFlow", payload)

You can adopt it bit by bit. Flows live alongside any chain() / group() / chord() you already build by hand.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

celeryflow-0.1.1.tar.gz (20.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

celeryflow-0.1.1-py3-none-any.whl (16.2 kB view details)

Uploaded Python 3

File details

Details for the file celeryflow-0.1.1.tar.gz.

File metadata

  • Download URL: celeryflow-0.1.1.tar.gz
  • Upload date:
  • Size: 20.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for celeryflow-0.1.1.tar.gz
Algorithm Hash digest
SHA256 26f55cc61e2027aff8ff66d30b805221ae1b4f60b8cf37358c78c48a9d6d0a6f
MD5 5fecf716bf1e94948bcac758788b2cd9
BLAKE2b-256 3f551302c49b2d8a2493eb22cf5a0cdac19609b8a5af29d248ea39ce547fd1a0

See more details on using hashes here.

Provenance

The following attestation bundles were made for celeryflow-0.1.1.tar.gz:

Publisher: release.yml on ChenYuTingJerry/CeleryFlow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file celeryflow-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: celeryflow-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 16.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for celeryflow-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4085cf96806398381914e9924b9311d77219d18fe091a5d1530f2c1eabff8c53
MD5 0dd501648ab896a04b8629e27dc080ff
BLAKE2b-256 60e03518681222d8afcf0506571d25d3285b5a32c61df4ccd93fde49a7bc1d7f

See more details on using hashes here.

Provenance

The following attestation bundles were made for celeryflow-0.1.1-py3-none-any.whl:

Publisher: release.yml on ChenYuTingJerry/CeleryFlow

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page