Skip to main content

Workflow engine with handler registration, channel-based I/O, and remote execution

Project description

Antkeeper

A lightweight Python workflow engine for agentic workflows that run AFK.

Who This Is For

Engineers building agentic workflows — LLM-driven pipelines that spec out features, write code, create branches, open PRs — that need to run reliably without supervision.

Once you're running similar workflows across multiple projects, the duplication in wiring, error handling, and state management becomes unmanageable. Antkeeper extracts all of that into a framework so you can focus on the application-specifics of your agentic layer.

The Problem

As you build with AI agents, you accumulate scripts that chain LLM calls into workflows. A typical one might take a prompt, generate a spec, create a branch, implement a feature, then document the changes. These scripts tend to:

  • Lose state — a failure mid-workflow means starting from scratch. No recovery, no way to run unattended.
  • Duplicate wiring — every script re-implements argument parsing, error handling, progress reporting, and LLM integration. Across repos, this compounds.
  • Lock to one surface — a CLI script cannot easily become a Slack bot or an API endpoint, but you need the same workflow from all three.
  • Bind to one agent — the workflow is hardwired to one LLM tool. Switching from Claude Code to Codex or a local model means rewriting.
  • Hide what happened — no logging, no observability, no progress tracking. When something fails at 2am, there is no trail.

Antkeeper solves all five. State is persisted after every step. Handlers are reducers with no I/O coupling. Channels decouple trigger surfaces. The LLM layer is a protocol. And every workflow run produces file-based logs and optional OpenTelemetry traces.

How It Works

Antkeeper draws on two ideas:

From Flask — decorators register entry points and the framework handles the wiring. In Flask, @app.route("/hello") binds a function to a URL. In Antkeeper, @app.handler registers a function as a workflow step that can be triggered from any channel.

From Redux — state is a plain dictionary that flows through pure functions. Each handler receives the current state and returns a new one, like a reducer. Handlers never mutate state — they return a fresh dict. State is persisted after every step, so a failure at step 3 of 5 preserves the output of steps 1 and 2.

An SDLC pipeline that takes a prompt and produces a specified, implemented, documented feature on a new branch:

from antkeeper.core.app import App, run_workflow
from antkeeper.core.runner import Runner
from antkeeper.core.domain import State
from antkeeper.handlers.claude_code import cc_handler

app = App()

# Factory-built steps: run an LLM command and thread results through state
specify    = cc_handler("/specify $prompt", state_updates=["spec_file", "slug"])
implement  = cc_handler("/implement $spec_file")
document   = cc_handler("/document this branch.")

@app.handler
def branch(runner: Runner, state: State) -> State:
    """Create a feature branch from the slug the specify step produced."""
    from antkeeper.git import execute
    slug = state["slug"]
    execute(["checkout", "-b", slug])
    return {**state, "branch_name": slug}

@app.handler
def sdlc(runner: Runner, state: State) -> State:
    """Full pipeline: specify -> branch -> implement -> document."""
    return run_workflow(runner, state, [specify, branch, implement, document])
antkeeper run --model sonnet sdlc prompts/add-auth.md

The same workflow runs from an HTTP webhook or a CI job. With a Slack app configured, it can also be triggered by a message. The channel determines how input arrives and how progress is reported. Handlers work with state only.

Core Concepts

State is a plain Python dictionary (dict[str, Any]). Each handler receives the current state, does its work, and returns a new dict. State is persisted as JSON after every step. A failure mid-workflow preserves the output of all completed steps, and antkeeper resume <run_id> can restart from where it left off.

Handlers are workflow steps. They follow the reducer pattern: (Runner, State) -> State. Pure functions — state in, new state out, no mutation. Register with @app.handler to make them callable by name from any channel. For the common case of running an LLM command and extracting fields from the response, the cc_handler factory builds handlers declaratively.

Runner is the execution context for a single workflow run. It binds an App (handler registry) to a Channel (I/O boundary) and manages the run lifecycle: generating a unique run ID, setting up per-run file logging, persisting state, and resolving environment variables. Handlers use the runner to communicate back to the channel (runner.report_progress(), runner.report_error()) and to signal failure (runner.fail()). The runner is passed to every handler but handlers do not need to manage it — it is infrastructure.

Channels are I/O adapters. They decouple workflow logic from how it is triggered and how it reports progress. The CLI channel reads from the terminal and writes to stdout. The API channel accepts HTTP POST requests and runs workflows in the background. The Slack channel posts to threads (requires your own Slack app — see below). The Programmatic channel runs workflows directly in-process from Python code, returning the final state as a dict and routing events to optional callbacks. Handlers do not need to know which channel is active.

Agents are the LLM abstraction. Any object with a prompt(str) -> Iterator[StreamEvent] method qualifies. The built-in ClaudeCodeAgent delegates to the Claude CLI using --output-format stream-json, streaming events as they arrive. The protocol is deliberately minimal — plug in any backend. The cc_handler factory is a convenience method built on this protocol, but hand-written handlers can use it directly via run_prompt().

Built-in Integrations

Git is available out of the box. execute() runs git commands, current() returns the branch name, Worktree and git_worktree manage isolated working directories. Git is ubiquitous in agentic workflows, so it ships as a utility. The same pattern could be extended for other common tools.

Slack is supported as a channel. The framework includes a SlackChannel that posts workflow progress and results as thread replies, triggered by @mentions. However, using it requires a Slack app installed in your workspace. Antkeeper does not distribute a public Slack app — you would need to create your own. See Slack Integration for details.

Two Ways to Write Handlers

The decorator pattern provides full control:

@app.handler
def my_step(runner: Runner, state: State) -> State:
    runner.report_progress("doing work")
    return {**state, "result": "done"}

The handler factory (cc_handler) eliminates boilerplate for steps backed by Claude Code. Most agentic workflow steps interpolate state into a prompt, call an LLM, and extract structured fields from the response:

specify = cc_handler("/specify $prompt", state_updates=["spec_file", "slug"])

This creates a handler that interpolates $prompt from state, runs the command, extracts spec_file and slug from the response, and merges them into state.

Both approaches compose freely. Use the factory for the common pattern, hand-write handlers for custom logic.

Installation

git clone https://github.com/mowat27/antkeeper.git
cd antkeeper
uv sync

Quickstart

# Scaffold a new project with starter handlers
antkeeper init my-project
cd my-project

# Run the healthcheck workflow
antkeeper run healthcheck

# Run an LLM workflow with a prompt file
antkeeper run --model sonnet specify prompts/describe.md

# Run the full SDLC pipeline
antkeeper run --model sonnet sdlc prompts/add-auth.md

# Start the API server
antkeeper server --host 0.0.0.0 --port 8000

# Run with verbose output (all events as JSON instead of only progress/error)
antkeeper run --model sonnet --verbose sdlc prompts/add-auth.md

# Resume a partially-completed workflow (run_id shown in the original run output)
antkeeper resume a1b2c3d4

# Trigger via HTTP
curl -X POST http://localhost:8000/webhook \
  -H "Content-Type: application/json" \
  -d '{"workflow_name": "sdlc", "initial_state": {"prompt": "Add user authentication", "model": "sonnet"}}'

For Slack integration (requires your own Slack app), set SLACK_BOT_TOKEN and SLACK_BOT_USER_ID in a .env file and start the server. See Slack Integration.

Programmatic Usage

Run workflows directly from Python without a CLI or server:

from antkeeper.channels.programmatic import ProgrammaticChannel

channel = ProgrammaticChannel(
    on_progress=lambda run_id, event: print(f"[{run_id}] {event.content}"),
    on_error=lambda run_id, message: print(f"[{run_id}] ERROR: {message}"),
)

result = channel.run_handler(
    "my_workflow",
    {"prompt": "Add auth"},
    handlers_file="handlers.py",
)
print(result)  # final state dict

run_handler() is synchronous and self-contained. Each call loads the handlers file, runs the workflow, and returns the final state. WorkflowFailedError and all other exceptions propagate directly to the caller. Callbacks are optional — omit them to run silently. The handlers_file path is resolved relative to process CWD.

Design Principles

  • State is a plain dictdict[str, Any]. Serialisable, inspectable, recoverable. Predictability through constraints, not capability.
  • Handlers are reducers(Runner, State) -> State. No mutation. Testable in isolation, composable in sequence.
  • Runner is infrastructure — execution context, logging, state persistence, and channel communication. Handlers receive it but do not manage it.
  • Channels separate I/O from logic — handlers are unaware of their trigger surface.
  • Agent-agnostic — the LLM layer is a protocol (prompt(str) -> Iterator[StreamEvent]), not a binding to one tool.
  • Composition over inheritancerun_workflow folds state through a list of steps. No DAG scheduler, no base classes.
  • Convention over configuration — sensible defaults for log directories, state persistence, and worktree paths.

Requirements

  • Python >= 3.12
  • uv package manager (for development)

Further Reading

  • Reference Guide — Handlers, channels, LLM integration, git utilities, state persistence, logging, CLI commands
  • Instrumentation — Progress reporting, error handling, logging patterns, state persistence, workflow resume, OpenTelemetry tracing, Axiom querying
  • HTTP Server — Server architecture and endpoint design
  • Slack Integration — Bot configuration, event handling, thread-based replies
  • Testing Policy — Test structure, fixtures, patterns
  • Engineering Standards — Dependency philosophy, performance, no-singletons policy
  • Releasing — Packaging, dependencies, PyPI release process

Development

# Run all checks (default just target)
just

# Individual checks
just ruff    # Lint
just ty      # Type-check
just test    # Tests

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

antkeeper-0.1.7.tar.gz (37.3 kB view details)

Uploaded Source

Built Distribution

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

antkeeper-0.1.7-py3-none-any.whl (53.3 kB view details)

Uploaded Python 3

File details

Details for the file antkeeper-0.1.7.tar.gz.

File metadata

  • Download URL: antkeeper-0.1.7.tar.gz
  • Upload date:
  • Size: 37.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for antkeeper-0.1.7.tar.gz
Algorithm Hash digest
SHA256 f5a3e8dba845a43df87e61557b144cdaaf569ec38a94b8f784eff14f18a57b59
MD5 bf45df28b80feb4814eeffa50c671abc
BLAKE2b-256 b993af30c256e7980635a2e8e72297ffa5c9e3d1d8d442246c482bfc77f26133

See more details on using hashes here.

File details

Details for the file antkeeper-0.1.7-py3-none-any.whl.

File metadata

  • Download URL: antkeeper-0.1.7-py3-none-any.whl
  • Upload date:
  • Size: 53.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for antkeeper-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 251df09afae863ff96933c41b6e19ec4670cd9b95b1abc6e5a1e86b85a0d0d19
MD5 9bafa85e111858d86ceae5e34c55a914
BLAKE2b-256 e6855b656c7580c97c48985c341564e5749c1e858a4d707932809fdf644309f0

See more details on using hashes here.

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