Zero-infrastructure workflow orchestration with automatic checkpointing
Project description
TideLock
Zero-infrastructure workflow orchestration with automatic checkpointing.
TideLock lets you write data pipelines as DAGs of Python steps. Every step is automatically checkpointed — so when a step fails, you fix the bug and resume from the failed node, skipping everything that already completed. No database, no scheduler, no web UI, no cloud.
pip install tidelock
from tidelock.engine import Flow, PipelineState, cli, pipeline, step
class State(PipelineState):
records: list[dict]
@step("fetch")
def fetch(shared: State):
shared.records = [{"id": 1, "value": 42}]
@step("process")
def process(shared: State):
for r in shared.records:
r["value"] *= 2
@pipeline()
def construct():
return Flow(fetch >> process, state_cls=State)
if __name__ == "__main__":
cli()
python pipeline.py run
python pipeline.py resume run_20260101_120000
python pipeline.py inspect
Why TideLock?
Every workflow orchestration tool asks you to set up infrastructure before you can run a pipeline:
| Tool | Infrastructure needed |
|---|---|
| Prefect | Server + database |
| Metaflow | AWS stack (S3, metadata service) |
| Airflow | Database + scheduler + web server |
| Dagster | Dagit web UI + database + daemon |
| TideLock | Nothing — pip install and run |
If you're building a data pipeline on a single machine — doing ETL, batch
processing, research analysis, or LLM inference — you shouldn't need a platform
team to get crash recovery. TideLock is the middle ground between "a raw Python
script that loses all progress on failure" and "a full orchestration platform
that needs a docker-compose up."
How it works
Steps
A step is a function that receives shared state and returns a routing string
(or None for the default path):
@step("validate")
def validate(shared: State) -> str | None:
if shared.records:
return "has_data"
return None
Steps can be sync or async:
@step("fetch")
async def fetch(shared: State):
async with httpx.AsyncClient() as client:
shared.raw = await client.get("https://api.example.com/data")
Graph DSL
Edges are built with two operators:
Flow(
fetch >> validate, # default transition
(validate - "has_data") >> process, # conditional transition
(validate - "empty") >> fallback,
state_cls=State,
)
| Syntax | Meaning |
|---|---|
node_a >> node_b |
Default transition (step returns None) |
(node_a - "label") >> node_b |
Conditional transition (step returns "label") |
The start node is inferred automatically — it's the unique node that appears
only as a source, never as a target. Flow raises ValueError if ambiguous.
State
State is a Pydantic model. Two conveniences over plain BaseModel:
class State(PipelineState):
topic: str = "default"
queries: list[str] # auto default_factory=list
sources: dict[str, str] # auto default_factory=dict
results: pd.DataFrame | None = None
- Bare collections —
list,dict,setget automaticdefault_factory. - Field references —
State.queriesreturns aFieldReffor use with.map()and similar APIs, avoiding magic strings.
After every step the full state is serialized automatically:
- Plain Python values → msgpack
pd.DataFramefields → parquetNonefields are skipped
Checkpointing
Checkpoints are stored under .pipeline_runs/<pid>/<node_name>/:
.pipeline_runs/
run_20260602_091500/
metadata.json # node order + status
fetch/
_action.msgpack # routing value
records.msgpack # per-field checkpoint
process/
_action.msgpack
records.msgpack
On resume, completed nodes are skipped and their checkpoints loaded to
restore state. The source run is never modified — a new run directory
is created and pre-populated with checkpoints up to the chosen node.
Concurrent steps (split)
Run independent branches in parallel:
Flow(
plan >> split(fetch_news, fetch_filings, fetch_reports) >> merge,
state_cls=State,
)
- Branches run via
asyncio.gather— wall time equals the slowest branch. - Each branch checkpoints independently.
- On resume, completed branches are skipped and failed ones re-run.
Retry policies
# Shorthand
@step("gather", retries=3, retry_delay=2.0)
async def gather(shared: State): ...
# Full control
@step("gather", retry=RetryPolicy(
attempts=4,
delay=1.0,
backoff=2.0, # delay doubles each attempt
jitter=0.25, # ±25% random spread
on=RateLimitError, # only retry this exception
))
async def gather(shared: State): ...
CLI
run
Execute the pipeline from the start node:
python pipeline.py run
resume <pid>
Branch a previous run into a new run, skipping completed nodes:
python pipeline.py resume run_20260602_091500
With --from-node / -f to skip the interactive picker:
python pipeline.py resume run_20260602_091500 --from-node validate
inspect [pid]
Open the Textual TUI to browse node state with regex search:
python pipeline.py inspect
python pipeline.py inspect run_20260602_091500
The TUI shows completed (green) and failed (red) nodes. Selecting a node
displays its state — JSON for msgpack fields, a sortable table for parquet
fields. Press / to search, Esc to clear, q to quit.
Requirements
Python 3.12+
| Package | Role |
|---|---|
pydantic |
State schema and validation |
msgpack |
Fast binary serialization |
pandas + pyarrow |
DataFrame serialization (parquet) |
typer |
CLI |
textual |
TUI inspector |
questionary |
Interactive node picker |
Comparison
| TideLock | Prefect | Metaflow | Airflow | |
|---|---|---|---|---|
| Setup | pip install |
Server + DB | AWS stack | DB + scheduler + web UI |
| Checkpointing | Per-step, file-based | Task retries only | Per-step, S3 | None |
| Resume | resume <pid> -f <node> |
Manual retry | --origin-run-id |
None |
| State inspection | Terminal TUI | Web UI | Web UI + CLI | Web UI |
| Concurrent branches | split() via asyncio |
Task runners | @parallel |
PythonOperator pools |
| DataFrame support | Native (parquet) | Via XCom | Native (S3) | Via XCom |
| Scheduling | No | Yes | No | Yes |
| Distributed execution | No | Yes | Yes | Yes |
| Lines of code | ~1,200 | ~200,000+ | ~150,000+ | ~300,000+ |
TideLock is not a replacement for Prefect/Airflow in production — it's for the stage before that, when you're iterating on a pipeline locally and don't want to stand up infrastructure just to survive a crash.
Examples
| File | What it shows |
|---|---|
examples/split_pipeline.py |
Concurrent branches with split(), retry policies, resume on partial failure |
Run the example:
cd TideLock
uv run python examples/split_pipeline.py run
uv run python examples/split_pipeline.py inspect
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tidelock-0.1.0.tar.gz.
File metadata
- Download URL: tidelock-0.1.0.tar.gz
- Upload date:
- Size: 21.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d21c3bdfb2cb4194cf1143989077838af6eecbe5325a7c5b4076945093de7c5
|
|
| MD5 |
11cd2c8591b71f4978a91f1d8fdbd408
|
|
| BLAKE2b-256 |
cff62aeb63dc19ee58b859c706fd75c1040d008caa68646c1dbea90ba35cb641
|
Provenance
The following attestation bundles were made for tidelock-0.1.0.tar.gz:
Publisher:
publish.yml on Hippopoto0/TideLock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tidelock-0.1.0.tar.gz -
Subject digest:
7d21c3bdfb2cb4194cf1143989077838af6eecbe5325a7c5b4076945093de7c5 - Sigstore transparency entry: 1706119987
- Sigstore integration time:
-
Permalink:
Hippopoto0/TideLock@6c7ed8db8bea62a27c4bd13abbef9b3d1a3b0df2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Hippopoto0
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6c7ed8db8bea62a27c4bd13abbef9b3d1a3b0df2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file tidelock-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tidelock-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3de75250f6183a570202b41e62810b0893cc225ac4bd1c5341333ff5c12e1d5e
|
|
| MD5 |
61eb54e4e006e0b05b52237e5fb92066
|
|
| BLAKE2b-256 |
2818980a34097b1f61f74439981c6f9115b5a929ae5b481e62cad8b66b6fd8fa
|
Provenance
The following attestation bundles were made for tidelock-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on Hippopoto0/TideLock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tidelock-0.1.0-py3-none-any.whl -
Subject digest:
3de75250f6183a570202b41e62810b0893cc225ac4bd1c5341333ff5c12e1d5e - Sigstore transparency entry: 1706120319
- Sigstore integration time:
-
Permalink:
Hippopoto0/TideLock@6c7ed8db8bea62a27c4bd13abbef9b3d1a3b0df2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Hippopoto0
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6c7ed8db8bea62a27c4bd13abbef9b3d1a3b0df2 -
Trigger Event:
release
-
Statement type: