Skip to content

Domain SDKs

Functualize's capabilities are organized as standalone Domain SDK packages. Each SDK defines a capability class, provider protocol, shared types, testing doubles, and domain metadata — carrying no heavy dependencies.


Architecture

graph TD
    subgraph "Domain SDKs (lightweight)"
        ai["functualize-ai"]
        tasks["functualize-tasks"]
    end

    subgraph "Implementation Plugins (heavyweight)"
        pydantic["functualize-ai-pydantic"]
        local["functualize-tasks-local"]
    end

    subgraph "Storage (not a domain)"
        sqlite["functualize-substrate-sqlite"]
    end

    pydantic --> ai
    local --> tasks
Hold "Alt" / "Option" to enable pan & zoom

Domain SDKs define what a capability does (protocols, types). Implementation Plugins provide how it works (real backends, API clients).

Storage is not a domain SDK

There was a functualize-state SDK, and it was retired. A backend-agnostic key-value protocol can only offer the intersection of every backend, which is worth least exactly where having a real database is worth most. functualize-substrate-sqlite now supplies a StoreSubstrate — give me this document, put this document back, stop anyone else while I do both — and every store follows it. See contributor/adr/022-storage-is-a-substrate-not-a-key-value-domain.md and the Custom Substrate example.

Interactivity is not a domain SDK

Interactivity is presentation architecture (a surface stack, phase-scoped activation, per-job TTY/Live grants), not backend selection with one active provider — so it was evicted from the domain-SDK pattern and folded into core. See Interactivity and contributor/adr/001-surface-architecture-collapse.md.


Available Domains

Domain SDK Package Capability Class Default Plugin
AI functualize-ai AI functualize-ai-pydantic
Tasks functualize-tasks Tasks functualize-tasks-local

Storage is not in this table — see the note above. A job's own durable state is rc.state, and where functualize keeps its bookkeeping is a substrate.


Using Domain SDKs

In Jobs (DI Injection)

Capabilities are injected into job functions via type annotations:

from functualize.job import RunContext
from functualize_ai import AI, ToolScope

def analyze(ai: AI, rc: RunContext):
    result = ai.complete("Analyze this data...", response_model=Analysis)
    rc.log(f"Analysis: {result.summary}")

In Standalone Scripts

Use testing doubles directly without a project:

from functualize_ai.testing import MockAI
from functualize_tasks import MockTasks

ai = MockAI(responses={"*summarize*": "Short summary"})
tasks = MockTasks()

Testing

Each SDK provides testing doubles that work without implementation plugins:

from functualize_ai.testing import MockAI
from functualize_tasks.testing import MockTasks

For interactivity, functualize.testing provides AutoPrompt (and see the Surface/PromptCollector test doubles in tests/).

These doubles are suitable for unit testing job logic without network calls, databases, or API keys.


Writing a Custom Implementation

Implement the domain's provider protocol and register via entry point:

# my_plugin/_provider.py
from functualize_tasks import TaskProvider

class JiraTasks:
    def add(self, title, *, linked_to=None): ...
    def update(self, task_id, *, status=None, notes=None): ...
    def list(self, *, status=None, filter=None): ...
# pyproject.toml
[project.entry-points."functualize.tasks_providers"]
jira = "my_plugin:JiraPlugin"

Storage follows a different shape — one substrate, installed at APP_READY. See the Custom Substrate example for a complete walkthrough.


Auto-Selection

When only one implementation plugin is installed for a domain, functualize auto-selects it. No configuration needed.

When multiple implementations are installed, specify your choice in the config file:

[ai]
provider = "pydantic"

[tasks]
provider = "local"

Scaffolding

Generate new SDK or plugin packages:

func builtin scaffold add domain --name my-domain
func builtin scaffold add plugin --domain tasks --name jira