Functualize
Drop-in CLI framework for Python — auto-discovery, dependency injection, layered config, workflow graphs, and a plugin ecosystem.
A reusable Python CLI framework with auto-discovery, structured execution context, layered configuration, workflow graphs, and a plugin ecosystem.
Why Functualize?
Most CLI frameworks give you argument parsing and stop there. Functualize provides the full application lifecycle:
- No boilerplate discovery — drop a file in a directory and it becomes a command
- Dependency injection — declare what your job needs via type annotations, the framework wires it
- Config without ceremony — layered resolution means the same job works locally, in CI, and in production
- Workflow orchestration — DAGs with conditional edges and gates, not just sequential scripts
- Pluggable everything — swap state backends, add adapters (HTTP, Lambda, MCP), extend via entry points
If you're building internal tooling, deployment pipelines, or any multi-step automation that outgrows a shell script, Functualize gives you structure without locking you into a monolith.
Features
- Auto-discovery — Drop job files into a directory and they're automatically registered as CLI commands. Six configurable filters control what qualifies.
- Job Groups —
JOB_GROUPorganizes commands into hierarchies (func infra deploy).GroupOptionsdeclare flags shared by every job under a group. @jobdecorator — Declare metadata, visibility, dependencies, caching, and matrix parameterization on any job function.- Structured RunContext — Capability-based execution:
Log,Invoke,Prompt,Perf,State, plusFromJobfor declarative dependency injection andFromStepfor binding a gate tool to an earlier step's result. - Layered Configuration — Resolution chain with preset strategies (classic, twelve-factor, env-only, remote-first) and
.envfile support. - Declarative Job Config — Pydantic models drive CLI options, config resolution, and TUI form fields.
- Workflow Graphs — DAGs with
Step(func),Gate(name, awaits=Model, strategy=...), andEdge. Gates block for human or AI input;--prompt-gatesresolves them inline,--scope-idresumes blocked scopes. - Domain SDK Architecture — Pluggable capability domains (state, AI, tasks, interactivity) with swappable provider backends.
- Plugin System — Extend via Python entry points: lifecycle hooks, CLI commands, dynamic jobs, adapter plugins, and format providers.
- Built-in commands —
func builtin parallel(concurrent jobs),func builtin history(run log),func builtin env(config as env vars),func builtin shell-init(shell completions),func builtin workflow(inspect and resume gates). - Pinned exit codes — Stable, documented codes:
0success ·1job raised ·2usage/config error ·3refused pre-flight ·4stale check ·5blocked awaiting gate input. - Standalone Mode — Run single-file jobs with
func file.py function, or make them self-executing with PEP 723 shebang scripts (#!/usr/bin/env -S func). - Inline TUI — Bare
funcopens a smart command shell under your prompt: SmartBar readiness colors, autocomplete, and config panels showing where every value comes from (viafunctualize[cli]). - Scaffold Generator — Bootstrap new projects, jobs, plugins, and TUI screens with
func builtin scaffold. - Testing Utilities —
TestRunContext,CapturingLog,MockInvoke, and other test doubles for unit testing jobs in isolation.
Installation
Install the func CLI globally:
# With uv (recommended — isolated install, auto-manages PATH)
uv tool install "functualize[cli]"
# Or with pipx
pipx install "functualize[cli]"
# Or with pip (into current environment)
pip install "functualize[cli]"
Verify:
func builtin version
# Or use the longer alias:
functualize builtin version
Both func and functualize are the same command — use whichever you prefer.
Adding to a project (as a library dependency): use
uv add functualizeorpip install functualizeinside your project instead. The core library has no CLI dependencies — addfunctualize[cli]only if your project uses thefuncCLI or TUI.
How func finds jobs
The CLI operates in two modes:
| Command | Mode | Description |
|---|---|---|
func file.py [function] |
Single-file | Run a specific file directly |
func <job_name> |
CWD discovery | Find and run a job by name from the current directory |
func (no args) |
Discovery | List all discovered jobs |
What makes a .py file invocable?
A Python file qualifies as a job module when both conditions are met:
- Filename is not underscore-prefixed — files like
_helpers.pyor__main__.pyare skipped - Contains at least one public top-level function — checked via AST parsing (fast, no import needed)
Once a qualifying file is imported, every public function (non-underscore-prefixed, defined in that module) becomes a registered job command. Imported functions, classes, and private _helper() functions are ignored.
In single-file mode (func file.py [function]), the file is imported directly — the same public-function rule applies. If you omit the function name, func lists all available functions in the file.
Executable scripts (PEP 723)
A script can declare its own entry point and its own dependencies inline, then be run like any other program:
#!/usr/bin/env -S func
# /// script
# dependencies = ["httpx"]
#
# [tool.functualize]
# job = "fetch"
# ///
import httpx
def fetch(url: str, timeout: float = 5.0) -> None:
print(httpx.get(url, timeout=timeout).text)
chmod +x fetch.py
./fetch.py https://example.com --timeout 2
url has no default, so it is a positional argument; timeout has one, so it is --timeout. Same rule as every other job — ./fetch.py --help shows the resulting usage line.
Two things are doing work here:
[tool.functualize] jobnames the function the file runs. Without it,funcreads the first argument as a function name — fine when you are exploring a file (func fetch.py fetch), wrong for a script, where./fetch.py https://example.comwould look for a function calledhttps://example.com. Declaring the job means the file is that job, and everything on the command line belongs to it.dependenciesis standard PEP 723. If any are missing from the current environment,funcre-runs the script throughuv runwith them installed. No virtualenv to create, norequirements.txtto keep in sync.
env -S is what splits func from the filename; a plain #!/usr/bin/env func also works, since there is nothing to split.
CWD discovery
In CWD discovery mode, func locates job directories in this order:
- Explicit config —
pyproject.toml[tool.functualize].jobs_directories, a.functualize.toml, or the global config (~/.config/functualize/config.toml) - Convention directories — a
.functualize/jobs(pluslibandplugins) directory, when present - CWD scan — by default only the current directory itself is scanned for qualifying
.pyfiles. Opt in to a deeper scan withfunc --discovery-depth N(0–5 levels) or persist it in config:
# pyproject.toml (optional — scan subdirectories for jobs)
[tool.functualize.discovery]
scan_depth = 2
Skipped directories: .venv, __pycache__, .git, node_modules, dist, build, and any dot-prefixed directory.
Skipped files: test_*.py, *_test.py, conftest.py, setup.py, and __init__.py.
# pyproject.toml (optional — explicit job directories)
[tool.functualize]
jobs_directories = ["src/myapp/jobs", "scripts"]
Quick Start
Functualize scales from a single script to a full framework project. Start simple, graduate when you need more.
Step 1: Run a Python script
Write a function, run it with func. No project setup, no config files.
# weather.py
from functualize.job import RunContext
def forecast(rc: RunContext):
"""Check today's weather forecast."""
rc.log("Fetching forecast...")
rc.log("Tomorrow: 24°C, sunny")
# Run a function directly
func weather.py forecast
Runnable code:
examples/quickstart/step1_basic/
Step 2: Add typed configuration
Same domain, but now with validated parameters. Pydantic models become CLI options automatically:
# weather.py
from pydantic import BaseModel, Field
from functualize.job import RunContext
class ForecastConfig(BaseModel):
city: str = Field(description="City to check")
days: int = Field(default=3, ge=1, le=7, description="Days to forecast")
api_url: str = Field(default="https://weather.example.com", description="Weather API endpoint")
def forecast(config: ForecastConfig, rc: RunContext) -> str:
rc.log(f"Fetching {config.days}-day forecast for {config.city}...")
rc.log(f"Using API: {config.api_url}")
result = f"{config.city}: 24°C, sunny for the next {config.days} days"
rc.log(result)
return result
Config fields resolve from multiple sources (highest priority first):
# 1. CLI flags
func weather.py forecast --city Tokyo --days 5 --api-url https://api.prod.example.com
# 2. Environment variables (JOBNAME_FIELD convention)
export FORECAST_API_URL=https://api.staging.example.com
func weather.py forecast --city Tokyo
# 3. Config file (if a config.base.ini exists in the directory)
# [forecast]
# api_url = https://weather.example.com
# days = 3
Auto-discovery: When you have multiple job files, put them in a jobs/ directory and point func at it once in pyproject.toml:
myproject/
├── pyproject.toml
└── jobs/
├── weather.py
└── deploy.py
# pyproject.toml
[tool.functualize]
jobs_directories = ["jobs"]
cd myproject
func # Lists all discovered jobs
func forecast # Runs the forecast job directly (no filename needed)
No
pyproject.toml? A one-offfunc --discovery-depth 1scans one directory level below the CWD instead.
Runnable code:
examples/quickstart/step2_config/
Step 3: Invoke jobs with phase tracking
Jobs can invoke other jobs with rc.invoke(). Track progress with rc.track_phase():
# weather.py
from pydantic import BaseModel, Field
from functualize.job import RunContext
from functualize.types import RunStatus
class ForecastConfig(BaseModel):
city: str = Field(description="City to check")
days: int = Field(default=3, ge=1, le=7, description="Days to forecast")
api_url: str = Field(default="https://weather.example.com", description="Weather API endpoint")
def forecast(config: ForecastConfig, rc: RunContext) -> str:
rc.log(f"Fetching {config.days}-day forecast for {config.city}...")
return f"{config.city}: 24°C, sunny for the next {config.days} days"
def alert(config: ForecastConfig, rc: RunContext):
"""Check forecast and send alerts if needed."""
rc.log("Checking alert conditions...")
rc.log("No severe weather — all clear")
def morning_report(config: ForecastConfig, rc: RunContext):
"""Run the full morning weather pipeline."""
rc.track_phase("forecast", "Fetching forecast", RunStatus.RUNNING)
rc.invoke("forecast", city=config.city, days=config.days)
rc.track_phase("forecast", "Forecast retrieved", RunStatus.SUCCESS)
rc.track_phase("alerts", "Checking alerts", RunStatus.RUNNING)
rc.invoke("alert", city=config.city)
rc.track_phase("alerts", "Alerts checked", RunStatus.SUCCESS)
rc.log("Morning report complete")
func weather.py morning_report --city Tokyo --days 5
Install the flow-viz plugin to see a live execution tree — zero code changes to your jobs:
pip install "functualize[cli]" functualize-flow-viz
⏳ morning_report
├─ ✓ forecast — Forecast retrieved (0.1s)
├─ ✓ alerts — Alerts checked (0.1s)
└─ ✓ morning_report (0.3s)
The plugin subscribes to invoke_start, invoke_end, and phase_change events automatically — rendering phase status, durations, and nested invocations without touching job code.
Runnable code:
examples/quickstart/step3_invoke/
Step 4: Browse and run jobs interactively
As your jobs/ directory grows, stop memorizing names and flags. Run bare func in a terminal and the inline TUI opens — a smart command shell rendered under your prompt (not fullscreen):
cd myproject
func
- SmartBar readiness — the command bar's border tells you the state at a glance: grey (no job) → yellow PENDING (required args missing) → green READY (executable) → red INVALID
- Tab — autocomplete job names, flags, and values (enum choices complete after a trailing space)
- Ctrl+Enter — execute in place; log output streams below the bar, and the shell scrollback stays intact
- Ctrl+R — the config panel ring: every config field with its effective value and where it came from (CLI flag, env var, config file, or default)
- Ctrl+E — the general ring: browse all discovered jobs and TUI settings
Requires the CLI extra (pip install "functualize[cli]"). Inline rendering works on Linux/macOS; on Windows, Textual falls back to a fullscreen driver. See the Inline TUI reference for the complete keybinding tables.
Runnable code:
examples/quickstart/step4_tui/
Step 5: Add AI with structured output
If you haven't already, install the CLI extras first:
pip install "functualize[cli]"
Then install the AI domain SDK:
pip install functualize-ai-pydantic
functualize-ai-pydanticpulls infunctualize-ai(the protocol) automatically. When only one AI provider is installed, it's auto-selected — no config needed.
Set your API key (the PydanticAI provider uses LiteLLM, which supports OpenAI, Anthropic, and others):
export OPENAI_API_KEY=sk-...
# Or: export ANTHROPIC_API_KEY=sk-ant-...
Now add an AI-powered job that uses rc.invoke() to call other jobs properly:
# weather.py (add to the same file)
from functualize_ai import AI
class TravelPlan(BaseModel):
destination: str
best_days: list[str]
packing_tips: list[str]
def travel_plan(config: ForecastConfig, ai: AI, rc: RunContext):
"""AI generates a structured travel plan from weather data."""
# Use invoke to get forecast (goes through lifecycle, hooks, plugins)
result = rc.invoke("forecast", city=config.city, days=config.days)
plan = ai.complete(
f"Create a travel plan for {config.city} based on: {result.return_value}",
response_model=TravelPlan,
)
rc.log(f"Best days: {', '.join(plan.best_days)}")
rc.log(f"Pack: {', '.join(plan.packing_tips)}")
func weather.py travel_plan --city Tokyo --days 5
Runnable code (works without API keys via
MockAI):examples/quickstart/step5_ai/
Step 6: Expose jobs to AI agents via MCP
Make your jobs callable by external AI agents (Claude, Cursor, Goose) using the MCP protocol:
pip install "functualize[cli]" functualize-mcp
Mark jobs for external visibility with @job:
from functualize.job.decorators import job
@job(
extra_description="Get a weather forecast for a city",
visibility="external",
tags=["weather", "safe"],
)
def forecast(config: ForecastConfig, rc: RunContext) -> str:
...
@job(
extra_description="Generate an AI travel plan based on weather data",
visibility="external",
tags=["weather", "ai"],
)
def travel_plan(config: ForecastConfig, ai: AI, rc: RunContext):
...
Serve your jobs as MCP tools:
func mcp serve
Using with Claude Code: Add the MCP server to your Claude config:
{
"mcpServers": {
"weather": {
"command": "func",
"args": ["mcp", "serve"],
"cwd": "/path/to/your/project"
}
}
}
Now Claude can discover and call forecast and travel_plan directly, passing structured config and receiving typed results. Jobs with visibility="internal" are hidden from MCP.
Runnable code:
examples/quickstart/step6_mcp/— full tool-surface reference inplugins/functualize-mcp/examples/
Step 7: Workflow checkpoints for AI agents
Use declarative workflows with gates to create bounded, multi-turn flows that AI agents can drive:
# weather.py
from functualize.workflow import workflow, Step, Gate, Edge, END
from pydantic import BaseModel, Field
class TripPreferences(BaseModel):
budget: str = Field(description="Budget level: budget, mid-range, luxury")
interests: list[str] = Field(description="Travel interests")
@workflow(
steps=[
Step(forecast),
Gate(name="preferences", awaits=TripPreferences,
tools=["run_job"], strategy="ai_outbound"),
Step(travel_plan),
],
edges=[
Edge(source="forecast", target="preferences"),
Edge(source="preferences", target="travel_plan"),
Edge(source="travel_plan", target=END),
],
)
def trip_planner(config: ForecastConfig, rc: RunContext) -> str:
"""Multi-step trip planning that pauses for AI input."""
rc.log(f"Itinerary for {config.city} complete.")
return f"Itinerary ready for {config.city}"
The graph is validated at decoration time. Jobs are registered as Step(func); pause points are Gate(name=..., awaits=Model). The decorated function's body is the epilogue — it runs once after the walk reaches END.
Three gate interaction modes:
| Mode | Flag / Strategy | Behavior |
|---|---|---|
| Blocked + Resume | (default) | Walk stops at gate (exit 5). Deposit input via func builtin workflow resume, resume via func --scope-id <id> trip-planner. Best for scripts, CI, and MCP agents. |
| Interactive Prompt | --prompt-gates or strategy="prompt" |
Gate prompts inline on a TTY. Walk completes in one invocation. Falls through to block when piped. |
| AI Agent | strategy="ai_outbound" |
Gate blocks for external AI deliberation via MCP. Agent discovers, inspects state, and deposits input via resume_gate tool. |
# Blocked (default) — two-step: deposit input, then resume
func trip-planner --city Tokyo
# → exit 5: "Blocked: gate 'preferences' in scope 'abc123'"
func builtin workflow resume abc123 preferences --input '{"budget":"mid-range"}'
func --scope-id abc123 trip-planner --city Tokyo
# Interactive — one invocation, gates prompt inline
func --prompt-gates trip-planner --city Tokyo
# → forecast runs → "Budget level?" → "Interests?" → travel-plan runs → exit 0
# AI agent — serve via MCP, agent drives the gate
func mcp serve
# → Claude discovers, inspects state, calls resume_gate, resumes scope
When served via MCP (func mcp serve), functualize-mcp exposes workflow tools that let an AI agent drive paused workflows:
- Discover —
list_active_workflows()shows paused or running workflows - Inspect state —
get_workflow_state(id)shows the current step, pending input model, and available tools - Resume —
resume_gate(id, {"budget": "mid-range", "interests": ["food", "culture"]})validates the input againstTripPreferencesand advances the workflow - Continue multi-turn — each gate creates a natural checkpoint where the agent reflects and decides
This creates bounded AI workflows — the agent operates within defined steps rather than open-ended execution.
Runnable code:
examples/quickstart/step7_workflow/— full workflow walkthrough inplugins/functualize-mcp/examples/
Step 8: Scaffold and distribute as a CLI
When your jobs grow into a real project, scaffold and install it as a standalone command:
func builtin scaffold init weather-app
cd weather-app
uv sync
This generates:
weather-app/
├── pyproject.toml # [project.scripts] entry point
├── README.md
├── config.base.toml
├── config.dev.toml
├── config.prod.toml
└── src/weather_app/
├── __init__.py
├── main.py # FunctualizeApp wiring
└── jobs/
├── __init__.py
└── sample_job.py
Move your weather jobs into src/weather_app/jobs/weather.py. The pyproject.toml declares a CLI entry point:
[project.scripts]
weather-app = "weather_app.main:run"
Install it as a global command (no uv run prefix needed):
# Install globally with uv tool (isolated, on PATH)
uv tool install -e .
# Now callable directly
weather-app forecast --city Tokyo --days 5
weather-app travel-plan --city Paris
Or with pip:
pip install -e .
weather-app --help
MCP works in project mode too — add functualize-mcp as a dependency in your pyproject.toml, and the plugin is auto-discovered at boot via entry points:
weather-app mcp serve
The MCP plugin registers its commands (mcp serve, mcp start, mcp stop, mcp tools, mcp list, mcp schema) automatically when installed. Your project's CLI exposes them alongside your job commands.
Walkthrough:
examples/quickstart/step8_scaffold/— the finished project lives inexamples/project/weather_app/
For the full progression guide (directory mode, library mode, adapter mode), see the Modes documentation.
Layered Configuration
Every JobConfig field resolves from multiple sources automatically. Same job, different environments — zero code changes:
# jobs/sync.py
from pydantic import BaseModel, Field
from functualize.job import RunContext
class SyncConfig(BaseModel):
api_url: str = Field(description="Target API endpoint")
batch_size: int = Field(default=100, description="Records per batch")
timeout: int = Field(default=30, description="Request timeout in seconds")
def data_sync(config: SyncConfig, rc: RunContext):
rc.log(f"Syncing from {config.api_url} (batch={config.batch_size})")
Three ways to provide config — they layer with clear priority. The job is
data-sync: names are canonical lowercase-hyphenated, derived from the Python
function name (def data_sync). Environment variables use underscores because
shells cannot export a hyphen, and a config section is accepted either way:
# 1. CLI flags (highest priority)
func data-sync --batch-size 2000
# 2. Environment variables (JOBNAME_FIELD convention)
export DATA_SYNC_BATCH_SIZE=500
export DATA_SYNC_API_URL=https://api.prod.example.com
# 3. Config files (base + environment overlay)
# config.base.ini
# [data_sync]
# api_url = https://api.example.com
# batch_size = 100
Resolution order: CLI → Env vars → Config file → Model defaults. The same job works locally, in Docker, and in production without any code changes — just swap the config source.
Config files use a base + environment overlay pattern. The active environment — FUNCTUALIZE_ENV, else ENVIRONMENT, else ENV, defaulting to dev — determines which overlay is merged on top of the base (matched case-insensitively):
# config.base.ini — always loaded
[data_sync]
api_url = https://api.example.com
batch_size = 100
# config.prod.ini — merged on top when ENVIRONMENT=prod
[data_sync]
api_url = https://api.prod.example.com
batch_size = 500
# Local dev (default) — uses config.base.ini + config.dev.ini
func data-sync
# Production — uses config.base.ini + config.prod.ini overlay
ENVIRONMENT=prod func data-sync
| Preset | Strategy | Best for |
|---|---|---|
classic() |
CLI → Env → Config files → Defaults | Local dev, desktop tools |
twelve_factor() |
CLI → Env → Defaults | Docker, Kubernetes |
env_only(dotenv=True) |
CLI → Env → Defaults | Serverless, minimal setups |
remote_first() |
CLI → Remote → Env → Files → Defaults | Vault, AWS Secrets Manager |
Presets are selected in your project's main.py when constructing FunctualizeApp:
from functualize.app import FunctualizeApp, JobSources, twelve_factor
app = FunctualizeApp(
name="weather-app",
job_sources=JobSources(directories=["weather_app.jobs"]),
config_sources=twelve_factor(), # Env-only for Docker/K8s
)
Note: When using
funcCLI in single-file mode, the default preset (classic()) is always used — presets only apply to scaffolded projects with amain.py.
Environment Variables and .env Files
Functualize reads environment variables from os.environ during config resolution. A .env file can inject values into os.environ before resolution runs — controlled by ConfigSources.dotenv / dotenv_path for apps, and by the resolved CLI config plus --dotenv-file / --no-dotenv for the func CLI:
# Explicit .env loading — injects into os.environ before config resolution
myapp --dotenv-file .env data_sync
# App boot honors ConfigSources.dotenv (the dataclass default is True):
# a ./.env in the working directory is loaded at boot. Use
# ConfigSources(dotenv=False) or the twelve_factor() preset to disable.
myapp data_sync
Key points:
- The
ENVIRONMENTvariable (from shell or.env) controls which config overlay file is selected. If your.envsetsENVIRONMENT=prod, the app loadsconfig.prod.inion top ofconfig.base.ini - Shell environment variables always take precedence over
.envfile values (python-dotenv does not override existing vars by default) - The effective resolution priority: CLI flags > Shell env vars >
.envfile values > Config files > Model defaults - Only the current working directory's
.env(or an explicitdotenv_path) is considered — there is no upward directory scan, so a.envin a parent directory is never silently picked up - The
funcCLI defaults todotenv = false; opt in per project via[tool.functualize] dotenv = true,FUNCTUALIZE_DOTENV=true, or--dotenv-file
Because .env is loaded into os.environ before the config system reads it, .env can influence both the config values (via JOBNAME_FIELD env vars) and which config files are loaded (via the ENVIRONMENT variable).
Reproducibility tip: Automatic
.envloading can cause hard-to-debug differences between environments. For CI and production, usetwelve_factor()/ConfigSources(dotenv=False)(or--no-dotenvon the CLI) so environment variables come only from the orchestrator.
Extending with Plugins
Plugins are standalone packages that extend any Functualize app via Python entry points. Install one and it's active immediately — no code changes in the host app.
Writing a plugin
A plugin is a class with metadata attributes and a __call__(app) method:
# src/functualize_metrics/__init__.py
class MetricsPlugin:
name = "metrics"
version = "1.0.0"
description = "Emit job execution metrics to StatsD"
def __call__(self, app) -> None:
"""Called at boot — hook into lifecycle events."""
@app.before_job
def on_start(job_name, config):
statsd.increment(f"job.{job_name}.started")
@app.on_job_success
def on_success(job_name, result, duration):
statsd.timing(f"job.{job_name}.duration", duration)
@app.on_job_failure
def on_failure(job_name, error):
statsd.increment(f"job.{job_name}.failed")
Register via entry point in pyproject.toml:
[project.entry-points."functualize.plugins"]
metrics = "functualize_metrics:MetricsPlugin"
Once installed (pip install functualize-metrics), the plugin is auto-discovered at boot. Every job in every Functualize app gets metrics automatically.
Registering CLI commands from a plugin
Plugins can add sub-commands to the host CLI:
class DBPlugin:
name = "db-tools"
version = "1.0.0"
description = "Database management commands"
def __call__(self, app) -> None:
def migrate(target: str = "head"):
"""Run database migrations."""
print(f"Migrating to {target}")
def seed(count: int = 100):
"""Seed sample data."""
print(f"Seeding {count} records")
app.register_plugin_command("migrate", migrate, group="db", help_text="Run migrations")
app.register_plugin_command("seed", seed, group="db", help_text="Seed data")
This creates my-app db migrate and my-app db seed commands.
Registering dynamic jobs
Plugins can register jobs that become invocable via rc.invoke() and visible in the CLI:
class HealthPlugin:
name = "health-monitor"
version = "1.0.0"
description = "Registers a health check job"
def __call__(self, app) -> None:
def check_health(config, rc):
"""Check endpoint health."""
import httpx
resp = httpx.get(config.endpoint, timeout=config.timeout)
rc.log(f"Status: {resp.status_code}")
app.register_dynamic_job(
name="health-check",
function=check_health,
config_class=HealthCheckConfig,
group="monitoring",
)
Dynamic jobs are fully functional — invocable via rc.invoke("health-check"), visible in the TUI, and trigger lifecycle hooks.
Public API
| Package | Purpose |
|---|---|
functualize.app |
FunctualizeApp constructor, config presets, adapters |
functualize.job |
RunContext, capabilities (Log, Invoke, Prompt, Perf, State), @job decorator |
functualize.plugin |
EventBus, JobProvider, AdapterPlugin |
functualize.types |
JobResult, JobDescriptor, enums |
functualize.workflow |
@workflow, Step, Gate, Edge, ConditionalEdge, END |
functualize.testing |
TestRunContext, CapturingLog, MockInvoke |
See the full plugin and extension docs for lifecycle hooks, middleware, event bus, custom providers, and more.
Plugin Ecosystem
Install the full plugin ecosystem with a single command:
pip install "functualize[all]"
| Plugin | Purpose |
|---|---|
functualize-ai |
Provider-agnostic LLM interaction with budget enforcement and tool scoping |
functualize-ai-pydantic |
PydanticAI-backed AI provider with LiteLLM routing and structured output |
functualize-flow-viz |
Live inline execution tree visualization with step status and durations |
functualize-http |
HTTP delivery adapter exposing jobs as API endpoints via stdlib asyncio |
functualize-inline |
Textual-based inline terminal widgets for prompts, selections, and progress |
functualize-lambda |
AWS Lambda delivery adapter for serverless job execution |
functualize-mcp |
Model Context Protocol adapter exposing jobs as tools to AI agents |
functualize-state |
State domain SDK with protocols for key-value persistence and execution tracking |
functualize-state-sqlite |
SQLite-backed state persistence and execution history in WAL mode |
functualize-tasks |
Task management domain SDK with status tracking and event emission |
functualize-tasks-local |
Local state-backed task storage provider for the tasks domain |
Every plugin ships runnable examples in its own folder: plugins/<name>/examples/.
For plugin quality tiers and publishing guidelines, see plugins/PUBLISHING.md.
Requirements
- Python 3.11+
Development
# Clone and install
git clone https://github.com/raicing-ai/functualize.git
cd functualize
# Install tooling (mise manages python + uv versions)
mise install
# Sync dependencies (creates .venv, installs all workspace packages)
uv sync
# Run fast tests (unit only, skips property-based tests)
uv run pytest
# Run full test suite including property-based tests
uv run pytest --run-slow
# Run full suite with coverage and parallelism (mirrors CI)
uv run pytest --run-slow --cov=functualize -n auto
# Lint and format
uv run ruff check src/ tests/
uv run ruff format src/ tests/
# Type check
uv run mypy src/
# Architecture enforcement (import-linter)
uv run lint-imports
# Run all pre-commit hooks
uv run pre-commit run --all-files
See CONTRIBUTING.md for the full development guide.
Documentation
# Install docs dependencies
uv sync --group docs
# Live preview
uv run mkdocs serve
# Build (strict mode catches broken links)
uv run mkdocs build --strict
Docs deploy automatically to GitHub Pages on push to main.
Contributing
Contributions are welcome! Please read our Contributing Guide before submitting a PR.
- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Make your changes with tests
- Ensure CI checks pass (lint, type check, tests, import-linter)
- Open a Pull Request against
main
Changelog
See CHANGELOG.md for release history and migration notes.
Security
See SECURITY.md for reporting vulnerabilities.
License
MIT © Mohammad Hakim Adiprasetya
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 functualize-0.1.0.tar.gz.
File metadata
- Download URL: functualize-0.1.0.tar.gz
- Upload date:
- Size: 730.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d88847ff6b57a7618e1d055f108604e9c4d375275abea3601ea20709caa86f6b
|
|
| MD5 |
9786aefd7dfed394616012bb7e234e6f
|
|
| BLAKE2b-256 |
2a104f21eab54f8957d116eb83578cd373f3f9f1e86a870d69f8cfa6e29ccee5
|
File details
Details for the file functualize-0.1.0-py3-none-any.whl.
File metadata
- Download URL: functualize-0.1.0-py3-none-any.whl
- Upload date:
- Size: 897.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46175732f933e5b31d3b4bf14799d6d23a0e59bcd4f62be9f1acc296122f07f6
|
|
| MD5 |
ee63221787b21f49e11aad3361a87a40
|
|
| BLAKE2b-256 |
eed750b2b7bbb21bdf3c9b1011cac79c71818d90b2d3a54c14a55d9b24f20685
|