Skip to main content

Decorators

AgentFlow decorators are the source-code contract for system-owned capabilities. They run when Python modules are imported, register metadata in memory, and give tenant bootstrap enough desired state to converge each tenant database.
Decorators are a backend authoring surface. The Python and JavaScript SDKs manage runtime resources through HTTP APIs; importing framework.decorators in an SDK app does not register anything in the AgentFlow server.

Mental Model

Decorator registration is intentionally side-effect based:
  1. Startup imports agent classes, tools, prompt modules, artifact modules, and context resolvers.
  2. Decorators validate the class or function and register metadata in class attributes or registries.
  3. Tenant bootstrap reads those registries and upserts system data into the tenant database.
  4. Each request loads the tenant-scoped agent, tool, KB, skill, artifact, and prompt assignments from the database.
That split matters: decorators define code-owned desired state; the database holds the tenant-specific realized state.

Decorator Map

SurfaceWhere it livesWhat import-time registration storesWhat bootstrap/API uses
@agent(...)agents/*.pycls._agent_configSystem agent rows
@sub_agent(...)agents/*.pycls._agent_config, cls._parent_agent_names, optional approval configSystem sub-agent rows and agent_subagents edges
@tool(...)tools/**/*.pyGlobal tool registry entry and optional agent targetingTool definitions and agent assignments
@artifact(...)artifacts/<artifact_type>/ARTIFACT.pyArtifact default, summarizer, schemas, strict type registrationArtifact catalog rows and agent assignments
register_cached_tool(...)Usually next to @artifactTool-result summarizer mappingCached tool-result previews
@prompt_block(...)prompts/<name>/PROMPT.pyDynamic prompt block registrationPrompt assembly and context block rendering
static_block(...)Python modulesStatic prompt block registrationPrompt assembly
@entity_resolver(...)framework/context/resolvers/*.pyResolver for a context_refs[].system value@mention / context ref expansion
system_kb(...)KB seed modulesSystem KB specTenant KB rows, ingestion, and assignments

Backend vs SDK

Use decorators when the capability ships with the AgentFlow backend and should be reproducible from source on every deployment. Use the SDK or REST API when a tenant, admin, or end user creates or updates something at runtime.
GoalUse this
Ship a system agent class@agent or @sub_agent in the backend
Create a user-defined sub-agent at runtimeclient.agents.subagents.create(...)
Ship a source-controlled tool@tool in tools/
Register a tenant runtime toolclient.agents.tools.create(...)
Ship an artifact type and teaching prompt@artifact in artifacts/<type>/ARTIFACT.py
Create/update artifact instances in a conversationArtifact APIs
Ship prompt instructionsprompts/<name>/PROMPT.md or @prompt_block
Create editable runtime prompt textclient.prompt_blocks.create(...) or POST /api/v1/prompt-blocks

Startup And Bootstrap

Startup has two phases. Phase 1: tenant-agnostic import and registration The app imports agent implementation modules, imports code tools, imports prompt blocks, imports artifact defaults, and imports entity resolvers. Importing a module is what fires the decorators. Phase 2: tenant bootstrap For the startup tenant, or lazily on first request for any other tenant, bootstrap_tenant() reads the registered desired state and reconciles the tenant database:
  • Upserts system agents from AgentFactory classes carrying _agent_config
  • Deletes orphaned system agent rows whose code implementation was removed
  • Syncs agent_subagents from each sub-agent’s declared parents
  • Syncs code-managed KBs and agent KB assignments
  • Upserts shipped skills and artifacts and assigns them to default agents
  • Seeds tenant-default prompt block lists
  • Recomputes denormalized tool and sub-agent counts
Decorator code should therefore be import-safe: no network calls, no tenant-specific database access, and no work that belongs in request execution.

@agent

Use @agent for top-level system agents that inherit from framework.agents.agent.Agent.
from framework.agents.agent import Agent
from framework.decorators import agent


@agent(
    name="RevenueAgent",
    label="Revenue",
    description="Analyzes pipeline, accounts, and forecast risk",
    role="You are a revenue operations specialist.",
    blocks=["role", "communication", "tool_guidance", "error_handling"],
    llm_config={"model": "openai/gpt-5", "temperature": 0.2},
    reasoning_effort="medium",
    max_tokens=4000,
    enable_planning=True,
    enable_reflection=True,
    icon="chart-bar",
)
class RevenueAgent(Agent):
    pass
What the decorator does:
  • Validates the class inherits from Agent and not SubAgent
  • Builds an AgentConfig
  • Assembles a system prompt from system_prompt, or from role plus blocks and block_overrides
  • Stores the config on cls._agent_config
  • Leaves factory registration to agents/__init__.py
Common fields:
FieldPurpose
nameStable system name; this is what tools, prompts, KBs, and relationships target
label, icon, icon_typeUI metadata
descriptionHuman-readable capability description
system_promptFull prompt override when you do not want block assembly
role, blocks, block_overridesPrompt block assembly inputs
llm_configProvider/model and default model parameters
reasoning_effortAgent-level default reasoning depth for supported models
max_tokens, top_p, frequency_penalty, presence_penaltyAgent-level generation defaults
enable_planning, enable_reflectionCapability toggles
max_reflection_attemptsReflection retry ceiling
reasoning_effort is intentionally an agent config field, not only a per-request override. Request payloads can still override it for a single run.

@sub_agent

Use @sub_agent for specialized agents that inherit from framework.agents.sub_agent.SubAgent. Most sub-agents are callable by one or more parent agents; private/tool-only sub-agents can use parents=[].
from framework.agents.sub_agent import SubAgent
from framework.decorators import sub_agent
from framework.models.approval_models import ApprovalConfig


@sub_agent(
    name="ContractReviewAgent",
    label="Contract review",
    description="Reviews contract terms and prepares risk summaries",
    parents=["RevenueAgent", "MainAgent"],
    role="You review commercial contracts for revenue and renewal risk.",
    blocks=["role", "sub_agent_context", "communication", "tool_guidance", "citations"],
    llm_config={"model": "openai/gpt-5", "temperature": 0.1},
    reasoning_effort="high",
    require_approval=ApprovalConfig(enabled=True, timeout_seconds=600),
)
class ContractReviewAgent(SubAgent):
    pass
Use parents=[...] for the parent topology. There are no aliases: agent= and parent_agents= are intentionally rejected so sub-agent definitions have one spelling. The decorator normalizes names but does not verify that every parent exists; tenant bootstrap skips unresolved parents, so check scripts/inspect_agent.py or the agent manifest after adding a new relationship. What the decorator does:
  • Validates the class inherits from SubAgent
  • Builds and stores cls._agent_config
  • Stores parent agent names in cls._parent_agent_names
  • Stores optional approval configuration for the delegated sub-agent tool
  • Lets tenant bootstrap create the agent_subagents many-to-many edges
At runtime, each parent sees the sub-agent as a callable tool. Delegation creates a nested execution context with its own call_id, parent_call_id, prompt, tools, KBs, and event stream. The parent waits for the sub-agent’s final result before continuing.

Questions And Approvals In Sub-Agents

Sub-agents run a normal async agent loop. They can call tools that pause execution, including:
  • ask_question, which emits a question_required event and resumes after the user answers
  • Approval-gated tools, which emit an approval request and resume after approval, denial, cancellation, or timeout
Approval can be configured at two levels:
  • On the sub-agent decorator, which gates the parent-to-sub-agent delegation itself
  • On tools the sub-agent can call, which gates specific side effects inside the sub-agent run
Use an ApprovalConfig instance or equivalent dict when the default boolean is not precise enough:
@sub_agent(
    name="BillingAgent",
    parents=["MainAgent"],
    require_approval={
        "enabled": True,
        "timeout_seconds": 900,
        "auto_approve_after_success": 0,
        "escalate_on_timeout": True,
    },
)
class BillingAgent(SubAgent):
    pass

@tool

Use @tool for source-controlled backend functions that agents can call. Use agent="RecordsAgent" for one target agent, or pass a list to share the same code-defined tool across agents. The keyword is intentionally singular: agent= accepts str | list[str] | None.
from framework.decorators import tool
from framework.models.approval_models import ApprovalConfig


@tool(
    name="create_renewal_task",
    label="Create renewal task",
    description="Create a follow-up task for a renewal opportunity",
    agent=["RevenueAgent", "ContractReviewAgent"],
    tags=["tasks", "renewals"],
    group="Revenue Operations",
    require_approval=ApprovalConfig(enabled=True, timeout_seconds=300),
    source_type="tasks",
    show_in_timeline=True,
)
async def create_renewal_task(
    account_id: str,
    owner_id: str,
    due_date: str,
    note: str,
) -> dict:
    """Create a renewal follow-up task.

    Args:
        account_id: CRM account identifier.
        owner_id: User who owns the task.
        due_date: ISO date for the task.
        note: Task body.
    """
    ...
What the decorator does:
  • Reads the function name, docstring, type hints, and parameter descriptions
  • Creates a model-callable JSON schema
  • Creates or reuses the global Tool instance for the tool name
  • Records target agent names when agent= is supplied
  • Stores approval, icon, timeline, panel, source, tag, and group metadata
  • Returns a wrapper that still behaves like the original function
Tools still use agent= for target assignment. That name is historical, but it means “this tool is available to these agents”, not “this is a sub-agent parent”. require_approval=True uses default approval settings. Pass ApprovalConfig(...) or a dict for explicit timeouts and escalation behavior. All tools automatically receive a blocking execution-control argument in their model-facing schema. blocking=false queues a background tool job; the wrapped function does not receive that flag.

Streaming Tools

Tools may return a value or yield events from an async generator. This is how long-running tools can update the timeline and how ask_question can pause a run until a user responds.
from collections.abc import AsyncGenerator
from typing import Any

from framework.decorators import tool
from framework.models.executable import Event, EventType


@tool(agent="MainAgent", show_prompt_to_user=True)
async def request_missing_fields(fields: list[str], **kwargs) -> AsyncGenerator[Any, None]:
    call_id = kwargs["call_id"]

    yield Event(
        type=EventType.QUESTION_REQUIRED,
        call_id=call_id,
        content={"fields": fields},
    )

    answer = await wait_for_user_answer(call_id)
    yield {"fields": answer}
Framework-injected kwargs such as call_id, conversation_id, parent_call_id, and root_call_id should be treated as optional implementation details unless the tool explicitly needs them.

@artifact

Use @artifact to register a structured output type and its summarizer. Each artifact lives in artifacts/<artifact_type>/ARTIFACT.py.
from pydantic import BaseModel, Field

from framework.artifacts.action_specs import EventAction, StateAction
from framework.artifacts.specs import artifact
from framework.tools.result_summarizers import register_cached_tool


class AccountHealthContent(BaseModel):
    account_id: str
    account_name: str
    health_score: int
    risks: list[str] = Field(default_factory=list)


@artifact(
    artifact_type="account_health",
    label="Account Health",
    icon="heart-pulse",
    description="Account health summary with risks and recommendations",
    display_mode="panel",
    supports_streaming=True,
    agent=["RevenueAgent"],
    content_schema=AccountHealthContent,
    actions={
        "acknowledged": StateAction(timestamp_key="acknowledged_at"),
        "note_added": EventAction(),
    },
)
def summarize_account_health(content: dict) -> dict:
    return {
        "title": content.get("account_name", "Account health"),
        "score": content.get("health_score"),
        "risk_count": len(content.get("risks") or []),
    }


register_cached_tool(
    tool_name="get_account_health",
    summarizer=summarize_account_health,
)
Prefer artifact_type= for new code. The type is the canonical identifier, the XML tag name the model emits, the key in the summarizer registry, and the artifact catalog name. What the decorator does:
  • Validates the artifact type name
  • Registers catalog metadata such as label, icon, status, actions, and display mode
  • Registers the summarizer function for compact previews
  • Registers optional Pydantic content and event schemas
  • Adds the artifact to strict summarizer lookup
  • Reads a sibling PROMPT.md as the artifact teaching prompt when present, falling back to the summarizer docstring
Use register_cached_tool(...) when a tool returns large structured data. The full result stays in the result cache while the summarizer returns a compact preview plus cache reference to the LLM.

Prompt Blocks

Prompt blocks are discovered from prompts/<name>/. The folder name is the canonical block ID. Numeric prefixes such as 70-tool_guidance/ set render order and are stripped from the ID.

Static Blocks

Use PROMPT.md for stable instructions.
---
agents: ["RevenueAgent"]
category: "capabilities"
description: "How to handle renewal workflows"
---

When working on renewals, check open opportunities, account health, and recent emails before recommending next steps.
Allowed frontmatter fields are agents, order, category, and description.

Dynamic Blocks

Use PROMPT.py plus @prompt_block for local dynamic prompt blocks. The function returns body text; AgentFlow wraps it with the block name.
from framework.prompts.block_registry import PromptContext, prompt_block


@prompt_block(
    scope="conversation",
    tags=["renewals", "context"],
    ttl=300,
    agent=["RevenueAgent"],
)
async def renewal_context(ctx: PromptContext) -> str | None:
    data = await load_revenue_context(ctx)
    if not data:
        return None
    return format_renewal_context(data)
PROMPT.py must register exactly the canonical folder name. A folder cannot contain both PROMPT.md and PROMPT.py. Return None or blank text to skip the block. Use static_block(...) only when a static block genuinely needs to be declared from Python rather than a content file.

Entity Resolvers

Use @entity_resolver(system) to expand request context_refs into LLM context. Resolvers live under framework/context/resolvers/ and are imported during context discovery.
from framework.context.entity_resolvers import entity_resolver


@entity_resolver("crm")
async def resolve_crm_entities(refs: list[dict]) -> list[dict]:
    entities = []
    for ref in refs:
        entity = await crm_lookup(ref["type"], ref["id"])
        entities.append(
            {
                "system": "crm",
                "type": ref["type"],
                "id": ref["id"],
                "name": entity["name"],
                "content": format_entity_for_prompt(entity),
            }
        )
    return entities
Resolution groups refs by system, deduplicates refs, caps the total count, and runs resolver groups concurrently. Unknown systems, resolver failures, and resolver timeouts are logged and skipped so a broken resolver does not fail the entire chat request.

System KBs

Use system_kb(...) for code-managed knowledge bases that should exist in every tenant or in a tenant-specific seed set.
from framework.knowledge.system_kbs import system_kb


system_kb(
    id="550e8400-e29b-41d4-a716-446655440000",
    name="Revenue Playbook",
    description="Internal revenue workflow guidance",
    agents=["RevenueAgent", "MainAgent"],
    source_path="knowledge/revenue_playbook.md",
    tags=["revenue", "playbook"],
    icon="book-open",
    icon_color="green",
)
Tenant bootstrap upserts the KB, assigns it to the named agents, and auto-ingests source_path when configured and the KB is empty. For content-pack style KBs, create knowledge_bases/<name>/KB.yaml; seed code loads those templates and calls system_kb(...) during tenant bootstrap.

Content Discovery

Top-level framework content loads first. Customer content can be added with ADDITIONAL_CONTENT_PATHS, a comma-separated list of base directories. Each base may contain these subdirectories:
KindFolderCanonical fileNotes
Static prompt blockprompts/<name>/PROMPT.mdYAML frontmatter plus markdown body
Dynamic prompt blockprompts/<name>/PROMPT.pyMust call @prompt_block(name="<name>")
Artifact typeartifacts/<artifact_type>/ARTIFACT.pyUsually has sibling PROMPT.md
Skillskills/<name>/SKILL.mdFolder name is canonical identifier
Code-managed KBknowledge_bases/<name>/KB.yamlLoaded into system_kb(...) seed specs
Entity resolverentity_resolvers/*.py or context/resolvers/*.pyPython module with @entity_resolver(...)Imported during context discovery
Tools are currently discovered from the application repo’s tools/ package, not from ADDITIONAL_CONTENT_PATHS. Put customer-specific tools in the deployed application package or add an explicit import/discovery path before relying on @tool(...) side effects. Folders beginning with _ or . are skipped. Use _template/ folders for copy-starting-points. Name collisions should be treated as errors in source review. Framework/customer collision handling is intentionally strict or warn-loud depending on the discovery path, but production code should not rely on shadowing an earlier definition.

Validation And Strict Mode

AgentFlow validates decorator content in several layers:
ValidationFailure behavior
@agent class does not inherit from AgentRaises during import
@agent used on a SubAgent subclassRaises during import
@sub_agent class does not inherit from SubAgentRaises during import
Unknown AgentConfig fieldsRejected by Pydantic strict config
Tool module import failureApp startup logs by default; strict discovery helpers can raise when AGENTFLOW_STRICT_DISCOVERY=true
Prompt folder has both PROMPT.md and PROMPT.pyDiscovery error
Dynamic prompt registers the wrong nameDiscovery error and rollback
Prompt frontmatter has unknown keysDiscovery error
Artifact type name is invalidRaises during import
Duplicate artifact type with a different summarizerRaises during import
Artifact folder name and artifact_type mismatchStrict discovery failure; non-strict mode warns after import, so the wrongly declared type may already be registered
Artifact summarizer returns an invalid previewPreview validation failure
Content targets an unknown agentStartup validation warning with suggestions
Useful local checks:
uv run python scripts/inspect_agent.py MainAgent
uv run python scripts/inspect_artifact.py account_health
uv run pytest tests/unit/framework/test_prompt_discovery.py tests/unit/framework/test_artifact_discovery.py
Also inspect the runtime manifest for a deployed agent:
GET /api/v1/agents/MainAgent/manifest

Pitfalls

PitfallFix
Expecting a decorator to run without importing its modulePut the file under the discovered folder or import it from startup/factory code
Using SDK decorators to configure the backendUse backend decorators in the server repo, or SDK resource methods for runtime registration
Expecting an agent class to register without living under agents/Put the decorated class in a non-underscore agents/*.py module so startup discovery imports it
Doing I/O in a decoratorMove I/O to runtime execution, prompt block builders, KB ingestion, or bootstrap
Duplicating names across content packsRename the customer content; do not depend on shadowing
Returning huge tool payloads directly to the LLMPair a result-cache summarizer with an artifact type
Writing a dynamic prompt with a different name than its folderKeep folder name and @prompt_block(name=...) identical
Treating sub-agent approval as approval for every internal actionGate internal side-effect tools separately
The safe rule: decorators should be deterministic, import-safe declarations. Runtime behavior belongs in agents, tools, resolvers, prompt builders, KB sync, and APIs.