Plans
Plans make agent work explicit before it happens.
Plans make agent work explicit before it happens.
A plan is a typed container for the domain actions an agent intends to perform. Instead of letting an agent reason and mutate systems in the same loop, Flow AI lets the agent first produce a structured plan. The runtime can then validate, persist, inspect, approve, execute, and evaluate that plan.
Use plans when agent work is consequential: updating records, changing prices, approving requests, sending messages, triggering workflows, or modifying customer data.
The short version
A plan answers:
What is the agent going to do?
Why is it doing it?
Which domain actions will be executed?
What data or scope does the plan apply to?
Has the plan been approved?
What happened during execution?The normal flow is:
User goal
-> planner creates a typed plan
-> runtime validates and stores the plan
-> approval gate pauses risky execution
-> executor runs the approved actions
-> runtime records the resultPlans, actions, and tools
The important distinction is:
Plan = container for the proposed work
Action = domain-specific operation inside the plan
Tool = model-callable capability used for lookup, preview, or orchestrationFor an inventory or pricing workflow, a plan might contain actions like:
- change product price
- launch promotion
- reorder inventory
- disable unavailable product
- notify account manager
These are business-level actions that describe what should happen in the domain. The executor, runtime, and host application decide which tools, APIs, database operations, or workflows are needed to apply them.
Prefer domain actions:
Refund customer
Update subscription
Create support ticket
Send renewal email
Approve invoiceAvoid low-level implementation actions:
Call POST /refunds
Run SQL UPDATE
Invoke send_email_tool
Call CRM APIDefine typed actions
Model each action as a typed schema. When a plan can contain more than one
action type, use TaggedUnion(...) so every action has a discriminator.
from typing import Literal
from pydantic import BaseModel
from flowai_harness import TaggedUnion
class PriceChange(BaseModel):
kind: Literal["price_change"]
product_id: str
new_price: float
reason: str
class PromotionLaunch(BaseModel):
kind: Literal["promotion_launch"]
product_ids: list[str]
discount_pct: float
reason: str
class StakeholderNotification(BaseModel):
kind: Literal["stakeholder_notification"]
channel: str
message: str
reason: str
PricingAction = TaggedUnion(
PriceChange,
PromotionLaunch,
StakeholderNotification,
)TaggedUnion(...) defaults to the kind discriminator. It also supports the
harness shorthand kind: str = "price_change" if you prefer defaults over
Literal[...].
Define the plan body
The plan body is your application contract. It must contain a top-level
actions array, and it can include domain metadata such as rationale,
scope_ref, review notes, or requested constraints.
from pydantic import BaseModel, Field
from flowai_harness import define_plan
class PricingPlan(BaseModel):
scope_ref: str
rationale: str
actions: list[PricingAction] = Field(min_length=1)
pricing_plan = define_plan(
name="PricingPlan",
schema=PricingPlan,
)define_plan(...) creates a frozen PlanSpec. The schema can come from a
Pydantic model, JSON Schema, a simple {name: type} mapping, or another type
hint Pydantic can export.
Planner output uses the flat action shape from your schema:
{
"scope_ref": "catalog:summer-products",
"rationale": "Improve sell-through for slow-moving seasonal inventory.",
"actions": [
{
"kind": "price_change",
"product_id": "SKU-123",
"new_price": 19.99,
"reason": "The product is underperforming against similar items."
},
{
"kind": "promotion_launch",
"product_ids": ["SKU-123", "SKU-456"],
"discount_pct": 15,
"reason": "The products belong to the same seasonal campaign."
}
]
}When the planner calls storePlan, the runtime validates the whole body against
the PlanSpec schema. It then extracts actions, converts each flat action
into the canonical stored action shape, and keeps the other top-level fields as
plan context so they survive the round trip.
Planner and executor
Planning and execution are separate roles.
The planner creates and stores plan instances. The executor loads an existing plan and executes its approved actions.
Both roles should use the same PlanSpec:
from flowai_harness import define_executor, define_planner
planner = define_planner(
name="pricing_planner",
model="claude-sonnet-4-6",
plan=pricing_plan,
prompt="Create safe, typed pricing plans.",
)
executor = define_executor(
name="pricing_executor",
model="claude-sonnet-4-6",
plan=pricing_plan,
tools=[search_products, preview_price_change],
prompt="Execute approved pricing plans.",
)The harness scopes built-in plan tools by role:
- planners get
storePlanandgetPlan - executors get
getPlanandexecutePlan - other roles can read plans with
getPlanwhen they use the plans toolkit
This prevents the planner from directly executing plans and keeps the executor focused on approved stored work.
Approval and execution
Plans are a safety boundary between reasoning and side effects.
Without a plan, an agent may mix reasoning and writes:
Think -> call tool -> think -> call API -> update stateWith a plan, the agent commits to a typed proposal first:
Think -> store plan -> validate -> approve -> executePlan approval is configured through the runtime approval policy. When approval
is required, executePlan pauses before the write boundary. The plan remains in
draft while the approval gate is open; the runtime may emit a
pending_approval status-change event for display, but pending_approval is
not a stored plan status.
After approval, executePlan transitions the plan to execution and dispatches
the normalized actions through the host action_dispatcher.
def dispatch_actions(actions, ctx):
created = []
for action in actions:
if action["kind"] == "price_change":
payload = action["payload"]
created.append(
commerce_api.update_price(
product_id=payload["product_id"],
new_price=payload["new_price"],
)
)
else:
raise ValueError(f"unsupported action kind: {action['kind']}")
return {
"entitiesAffected": len(created),
"summary": f"Applied {len(created)} pricing action(s)",
"details": {"createdActions": created},
}Tools are model-callable. The action dispatcher is not model-callable; it is the
host callback that applies approved plan actions to your platform. A real
dispatcher should handle every action kind your PlanSpec allows, or reject
unsupported variants explicitly.
Lifecycle
Stored plans use a fixed lifecycle. Under the hood, each plan is a state machine: it can only move through valid transitions, and terminal states cannot be reopened.
draft -> approved -> executing -> executed
|
v
failedThe runtime owns these transitions:
storePlancreates adraftplan after schema validation.- The approval gate records approval and moves the plan to
approved. executePlanstarts the plan, runs the dispatcher, and recordsexecutedorfailed.
executed and failed are terminal statuses.
Display labels
You can customize how fixed lifecycle statuses appear to users:
pricing_plan = define_plan(
name="PricingPlan",
schema=PricingPlan,
display_aliases={
"draft": "Draft pricing proposal",
"approved": "Approved pricing proposal",
"executing": "Applying pricing changes",
"executed": "Pricing changes applied",
"failed": "Pricing update failed",
},
)Display aliases only affect presentation. The runtime still tracks the fixed
statuses draft, approved, executing, executed, and failed.
When to use plans
Use plans when work needs structure, review, approval, or replayable evidence of intent.
Good examples:
- changing prices
- launching promotions
- creating tickets
- updating customer records
- approving or rejecting requests
- running operational workflows
- sending external communications
- executing database-backed changes
Plans can also help with read-only workflows when structure matters:
- running an investigation
- generating a report
- comparing vendors
- preparing a migration proposal
- validating data quality
Plans are most valuable when the agent may modify state or trigger real-world effects.
Design guidelines
Good plan actions are:
- domain-specific
- typed
- inspectable
- executable
- easy to approve or reject
- stable enough to evaluate
Keep low-level mechanics in tools, dispatchers, and platform adapters. Keep the plan close to business intent.
Common mistakes
Do not use plans as free-form text summaries. A plan should describe executable intent in a typed shape.
Do not put write APIs directly in planner tools. Let planners inspect and propose; let executors and the host dispatcher apply approved actions.
See also
- Approvals for pausing plans before execution.
- Agents for planner and executor roles.
- Tools for model-callable capabilities.
- Action dispatcher for applying approved plan actions.
- Execute approved actions for applying plan actions to your platform.
define_planreference.
