Agent Surface
The agent surface is not a separate component — it is a property of every composable workflow. When a workflow is running, the agent surface exposes its current state, the unblocked work, any blockers, escalation paths, and an event stream. This is what makes a workflow agent-operable: an AI agent or automated system can drive the workflow forward without guessing what to do next.
We ship the surface. The agent belongs to you.
What "agent-operable" means
An agent-operable workflow gives structured answers to three questions at every point in time:
- Where are we? — The current process, what goals are open, what tasks are complete, what is still pending.
- What can happen next? — The unblocked goals and tasks in the current process, what each requires, and what tools are available.
- What is stuck? — Any blockers, what caused them, and what the recovery options are.
An agent that can answer these three questions can operate any workflow without workflow-specific code. The agent surface provides the answers as structured data, not prose or status strings.
Reading workflow state
At any point, the agent can read the full state of a running workflow:
from wishfleet import Wishfleet
wf = Wishfleet()
state = wf.flows.get_workflow_state("flow_abc123")
print(state.workflow.status)
# -> "open"
print(state.workflow.stage_name)
# -> "condition_documented"
print(state.processes)
# -> [
# {"name": "vacated", "status": "closed", "reason": "success"},
# {"name": "condition_documented", "status": "open"},
# {"name": "work_identified", "status": null},
# ...
# ]
print(state.goals)
# -> [
# {"public_id": "wf-abc123.con.d21485", "name": "document_condition",
# "status": "open", "required": True, "depends_on": []},
# ]
print(state.tasks)
# -> [
# {"public_id": "wf-abc123.con.d21485.1", "name": "photograph_unit",
# "status": "open", "depends_on": []},
# {"public_id": "wf-abc123.con.d21485.2", "name": "complete_checklist",
# "status": "open",
# "depends_on": ["wf-abc123.con.d21485.1"]},
# ]
The state is not a string — it carries the full tree of processes, goals, and tasks with their status, dependencies, and accumulated context. An agent reads this to understand where the workflow stands.
Unblocked work
The agent surface tells the agent exactly what goals and tasks are ready to act on in the current process, and what tools are available:
work = wf.flows.get_work_to_do("flow_abc123")
print(work.stage_name)
# -> "condition_documented"
print(work.unblocked_goals)
# -> [
# {
# "public_id": "wf-abc123.con.d21485",
# "name": "document_condition",
# "status": "open",
# "required": True,
# "depends_on": [],
# },
# ]
print(work.unblocked_tasks)
# -> [
# {
# "public_id": "wf-abc123.con.d21485.1",
# "name": "photograph_unit",
# "status": "open",
# "depends_on": [],
# },
# ]
print(work.tools)
# -> [
# {"name": "flag_access_issue", "description": "Report a problem accessing the unit"},
# ]
The agent does not need to know the workflow's domain logic. The surface tells it: here are the goals to achieve, here are the tasks to work on, here are the tools available for the current process. Goals and tasks with unsatisfied depends_on edges are filtered out — only actionable work appears.
Escalations and blockers
Some goals require human judgment — spending money, making policy decisions, resolving ambiguous situations. When the agent cannot proceed, the workflow surfaces structured information about what is needed:
state = wf.flows.get_workflow_state("flow_abc123")
# A goal is blocked waiting for human input
print(state.goals[2])
# -> {
# "public_id": "wf-abc123.wid.e34f01",
# "name": "approve_repairs",
# "status": "open",
# "required": True,
# "context": {
# "type": "approval_required",
# "reason": "Repair estimate ($2,400) exceeds agent spend limit ($500)",
# "requires": "landlord_approval",
# "options": ["approve", "reject", "request_alternative_estimate"],
# "estimate": 2400,
# "spend_limit": 500,
# "work_items": ["bathroom_tile_repair", "kitchen_faucet_replace"],
# },
# }
The blocker is not an error. It is a structured handoff to a human decision-maker. The workflow knows what question to ask, who to ask, and what the answer options are.
How escalations resolve
When a human makes the decision, the blocked goal is updated and the workflow resumes:
# Landlord approves via their own interface (RentRedi, email, etc.)
# The goal is updated and unblocked work reappears
work = wf.flows.get_work_to_do("flow_abc123")
# -> unblocked goals and tasks for the current process
The agent picks up where it left off. No special recovery code — the state machine handles the re-entry.
Event stream
The agent surface provides real-time events as the workflow progresses. An agent can subscribe to a workflow's event stream and react to changes:
for event in wf.flows.events("flow_abc123"):
print(event)
# -> {"type": "task_closed", "workflow": "wf-abc123",
# "node": "wf-abc123.wor.a1b2c3.1", "timestamp": "2025-04-03T16:00:00Z"}
#
# -> {"type": "goal_closed", "workflow": "wf-abc123",
# "node": "wf-abc123.wor.a1b2c3", "reason": "success"}
#
# -> {"type": "process_advanced", "workflow": "wf-abc123",
# "node": "wf-abc123.ver", "stage_name": "verified"}
Events are structured, timestamped, and tied to specific workflow nodes. An agent can use events to maintain awareness of multiple workflows without polling.
Authorization model
The workflow grammar defines what an agent is allowed to do. Authorization rules are declared per goal and per tool in the workflow specification:
agent— the agent can act autonomouslyhuman:{role}— requires approval from a specific role (landlord, property manager, tenant)spend:{limit}— the agent can proceed if the cost is within the spend limitpolicy:{rule}— a policy gate that must be satisfied (e.g., licensed contractor required for electrical work)
A goal or tool can carry multiple authorization clauses; all must be satisfied. When enforcement lands, the engine will re-validate on every write — the agent surface exposes authorization so the agent can decide whether to act or escalate before attempting an update.
Authorization is not bolt-on access control. It is part of the workflow grammar — the same specification that defines goals, tasks, and dependencies also defines who can act.
Worked example: wiring an LLM agent
Here is a pattern for an LLM agent that monitors and drives an apartment.close_and_prep workflow. The agent reads state, decides what to do, and updates goal and task status — handling blockers by escalating to humans.
from wishfleet import Wishfleet
wf = Wishfleet()
def agent_step(flow_id: str) -> None:
"""One decision cycle for the LLM agent."""
state = wf.flows.get_workflow_state(flow_id)
if state.workflow.status == "closed":
return
work = wf.flows.get_work_to_do(flow_id)
if not work.unblocked_tasks and not work.unblocked_goals:
return
# Work through unblocked tasks
for task in work.unblocked_tasks:
if needs_escalation(task, state):
escalate(task, state)
continue
result = execute_task(task)
wf.flows.update_task_status(
task_public_id=task["public_id"],
status="closed",
reason="success" if result.ok else "failed",
)
# Check if any goals can auto-close
for goal in work.unblocked_goals:
wf.flows.auto_close_goal(goal["public_id"])
def needs_escalation(task, state) -> bool:
"""Check whether the task exceeds the agent's authority."""
if task.get("estimated_cost", 0) > 500:
return True
return False
def escalate(task, state) -> None:
"""Route work that exceeds agent authority to the right human."""
notify_landlord(
flow_id=state.workflow.public_id,
task=task["name"],
reason=f"Estimated cost ${task['estimated_cost']} exceeds limit",
)
def execute_task(task):
"""Execute a single task. In production, this is
where the LLM reasons about domain context."""
return do_work(task)
The agent does not contain workflow-specific logic. It reads structured state from the agent surface, works through unblocked tasks, and lets the engine handle process advancement automatically when goals close. The workflow enforces the rules — dependency ordering, required goals, status grammar — so the agent can focus on judgment rather than orchestration.
This separation is the point: the agent surface makes workflows agent-operable without requiring agents to be workflow-aware.
Control-plane methods
The v2 engine exposes these methods through the agent surface:
| Method | What it does |
|---|---|
getWorkflowState | Full read: fetches all processes, goals, and tasks for a workflow |
getWorkToDo | Returns unblocked goals, unblocked tasks, and available tools for the current process |
updateTaskStatus | Sets a task's status and reason (success, failed) |
updateGoalStatus | Sets a goal's status and reason (validates dependency satisfaction on close) |
autoCloseGoal | Closes a goal with success if all non-deprecated tasks under it are closed with success |
addTask | Dynamically adds a task to a goal (validates name uniqueness, dependency references) |
deprecateTask | Closes an open task with reason: deprecated |
checkProcessAdvancement | Evaluates whether the current process is complete and advances or closes the workflow |
computeUnblockedWork | Pure function: given goals, tasks, and current process, returns those whose depends_on edges are satisfied |
publishSpecification | Validates and publishes an immutable workflow specification |
createInstance | Creates a running workflow from a specification and work item |
Evidence attachment, authorization enforcement, and the history/event log are described in the workflow grammar and covered in the Composable Workflows documentation.