Composable Workflows
A task is an atomic real-world action: measure a room, inspect for water damage, verify a lockbox. But real operations rarely stop at one action. A unit turn after a tenant moves out involves access confirmation, condition documentation, work identification, vendor routing, completion checks, and final verification — sequenced with rules about what evidence is needed, who can authorize each step, and what happens when reality misbehaves.
A composable workflow encodes that progression. You declare the processes the work passes through, the goals and tasks within each process, the evidence required, the exception paths, and the authorization rules. Wishfleet runs the model; you drive it forward via REST and MCP — or the decision engine drives it on your behalf.
Why composable
Static workflow templates are brittle. They assume a happy path and break when the world misbehaves — a contractor no-shows, unexpected damage is discovered, a landlord needs to approve an expense. A composable workflow treats these as first-class processes with defined recovery, not error conditions to handle in catch blocks.
Composable workflows also reuse the same task vocabulary. An inspect task inside a unit-turn workflow is the same inspect primitive used standalone. The workflow adds structure, evidence accumulation, and authorization — not a different programming model.
The application object model
Three layers compose how work moves through Wishfleet:
1. Work item (top, can carry concurrent workflows). A unit, a loan, a hire, a claim — the real-world subject. Multiple workflows may run against the same work item simultaneously. A property turn might have the turn workflow running alongside a separate compliance workflow. The workflows are independent in operation but share the same work-item context. The boundary between workflows is purpose: each workflow has its own definition, its own milestones, its own evidence requirements, its own terminal conditions.
2. Processes (middle, sequential within a workflow). Named milestones the work passes through one at a time: vacated, access_confirmed, condition_documented, and so on. The instance is in exactly one process at any moment. Re-entry to a prior process on recovery is normal (the recovery loop is what makes this a state machine, not a DAG). Concurrency across processes within the same workflow is not supported — that is what the workflow boundary is for.
3. Goals, tasks, and tools (bottom, concurrent + sequential within a process). Goals are declared outcomes within a process — "document condition," "assess severity," "scope repairs." Tasks are planned work that advances goals. Multiple goals can be open at the same time; sequencing comes from dependsOn edges between goals and tasks. Tools are available capabilities (escape hatches, manual actions) invokable while the process is active but not part of the planned dependency graph.
These three layers compose into a flow graph, not a flow line. An agent reading state must reason across all three at once: which workflows are open on this work item, which process each is in, which goals and tasks are unblocked and ready for work, and what tools are available. The agent surface is shaped to make all three layers queryable in a single view.
Grammar elements
Processes
A workflow moves through a sequence of named processes. Each process represents a meaningful milestone — not an implementation detail, but something a human operator or agent can reason about.
Processes are domain-specific. A unit-turn workflow has processes like vacated, access_confirmed, condition_documented, work_identified, work_assigned, work_completed, verified, and ready. A property-check workflow might have requested, scheduled, on_site, documented, reported.
Goals, tasks, and tools
Within a process, three kinds of things describe the work:
- Goals — what we want. Declared outcomes to be achieved within a process. Goals run concurrently by default; sequencing comes from
dependsOnedges. A goal may be markedrequired— if a required goal closes withreason: failed, the workflow halts. - Tasks — what we'll do. Planned units of work that advance goals. Tasks may be added, deprecated, or replaced as the workflow runs. Tasks link via
dependsOnedges (to other tasks or to goals), producing sequential or concurrent execution as the graph dictates. - Tools — what we can do. Available capabilities (interventions, escape hatches, manual actions) that aren't part of the planned task graph but may be invoked while the process is active.
Goal status is asserted explicitly — by a decision engine, a user action, or an external signal. Tasks are evidence of progress, not criteria for goal closure — closing all of a goal's tasks does not automatically close the goal unless the caller opts in. This keeps goal state stable in the face of mutable task plans.
The dependsOn cascade
When a goal declares dependsOn, the whole goal subtree respects it:
- The goal appears as unblocked work only when its own
dependsOnedges are satisfied. - The goal can close only when its
dependsOnedges are satisfied. - A task is unblocked only when both its own
dependsOnedges and its parent goal'sdependsOnedges are satisfied.
An author who writes staged.dependsOn = ["decluttered"] gets the natural reading — no task under staged runs, and staged itself cannot close, until decluttered is satisfied — without duplicating the dependency on every child task.
Status grammar
Goals, tasks, and workflow instances use a uniform status shape:
status—open|closedreason— set when closed. Vocabulary:success,failed,deprecated,skipped.
"Satisfied" means status == "closed" && reason == "success". There is no separate satisfied boolean or intermediate paused state.
Process advancement
Processes within a workflow run sequentially. Process N+1 begins when all goals in process N are closed. The engine checks advancement automatically after every goal status change:
- If any
requiredgoal closes withreason: failed→ the workflow closes withreason: failed. - If all goals in the current process are closed → advance to the next process (or close the workflow with
reason: successif this was the last process).
Advancement is a consequence of goal state, not a separately triggered event. There is no "fire a transition" action — the workflow advances when the work is done.
Evidence requirements
Evidence is structured proof captured during work — photos, attestations, measurements, documents, credit reports, automated findings. The workflow grammar describes evidence requirements per process: what typed artifacts must be present before the work can move forward.
Evidence is not free-text notes. It conforms to typed result profiles — a photo set with coverage requirements, a signed attestation, a measurement within tolerances. This is how physical truth enters software: as validated artifacts gated at the right moments, not bolted on later.
Exception paths
The real world does not follow happy paths. Exception processes handle the cases where reality diverges from plan:
- Provider no-show — the workflow detects a missed appointment and re-routes to a backup provider.
- Unexpected damage — an inspection reveals work that requires landlord approval before proceeding.
- Access denied — the provider cannot enter the unit; the workflow escalates.
- Budget exceeded — work estimates exceed authorized spend; the workflow pauses for approval.
Exception paths are not error handling. They are defined processes with their own goals, tasks, evidence requirements, and recovery paths. The workflow knows how to recover — re-entry to a prior process is first-class in the state machine model.
Authorization
Per-process rules covering who can authorize advancement:
agent— automated agent may trigger autonomously.human:<role>— requires sign-off from a specific role (landlord, property manager, tenant, underwriter).spend:<limit>— permitted if associated cost is at or below limit.policy:<rule>— a policy gate must be satisfied (licensed contractor, COI on file, regulatory check passed).
Multiple authorization clauses can apply; all must be satisfied. Authorization is exposed via the service API so callers can decide whether to act or escalate before attempting a write — but the workflow service re-validates on every write.
Worked example: unit turn readiness
The apartment.close_and_prep workflow takes a unit from vacated to ready_for_listing. Walking through it shows how processes, goals, tasks, and tools compose.
Start the workflow
POST /v1/workflow-instances
{
"accountId": "acct_abc123",
"workflowDefinitionId": "def_apartment_turn",
"workItem": {
"system": "property-mgmt",
"externalId": "unit-4b",
"displayName": "1240 Broadway, Unit 4B"
}
}
→ { "publicId": "wf-abc123", "currentProcess": "vacated", "status": "open" }
The process sequence
| Process | What it means | Typical goals |
|---|---|---|
vacated | Lease ended, tenant departed. | confirm_access, verify_keys_returned |
access_confirmed | A provider has verified they can enter. | schedule_inspection |
condition_documented | The unit's state has been recorded. | document_condition, assess_severity |
work_identified | Specific work items have been scoped. | scope_cleaning, scope_repairs, scope_paint |
work_assigned | Providers routed for each work item. | dispatch_cleaner, dispatch_repair, dispatch_paint |
work_completed | All dispatched work reports completion. | verify_cleaning, verify_repair, verify_paint |
verified | A verification visit confirms work. | final_walkthrough, photograph_completed |
ready | Confirmed ready for next tenant. | — |
Each process declares its goals and the tasks within them. Within a process, goals can run concurrently — for example, in work_assigned the cleaner, repair contractor, and painter may all be dispatched in parallel via separate goals.
Get work to do
GET /v1/workflow-instances/wf-abc123/work
→ {
"unblockedGoals": [
{ "publicId": "wf-abc123.cond.d21485", "name": "document_condition", "status": "open" }
],
"unblockedTasks": [
{ "publicId": "wf-abc123.cond.d21485.1", "name": "photograph_rooms", "status": "open" },
{ "publicId": "wf-abc123.cond.d21485.2", "name": "complete_checklist", "status": "open" }
],
"tools": [
{ "name": "request_maintenance", "description": "Escalate to maintenance team" }
]
}
The work-to-do response tells the agent (or operator) exactly what can be acted on right now — which goals are unblocked, which tasks are ready, and what tools are available.
Complete a task
PATCH /v1/workflow-tasks/wf-abc123.cond.d21485.1
{ "status": "closed", "reason": "success", "outcome": { "photoCount": 4, "coverage": ["kitchen","bathroom","bedroom","living_room"] } }
Close a goal
Once all tasks under a goal are complete, the goal can be closed — either explicitly or via auto-close (if the goal declares autoClose: true in the specification):
POST /v1/workflow-goals/wf-abc123.cond.d21485/auto-close
If all non-deprecated tasks under the goal are closed with reason: success, the goal closes with reason: success. Otherwise, auto-close is a no-op.
Once all goals in the current process are closed, the workflow automatically advances to the next process. Or let the decision engine drive the loop:
POST /v1/workflow-instances/wf-abc123/advance
Handling exceptions
At the work_assigned process, a contractor no-shows for a scheduled cleaning. The workflow detects the missed appointment (via a signal from your scheduling system or a timer firing past the SLA). A required goal closes with reason: failed, and the workflow enters a blocked process:
GET /v1/workflow-instances/wf-abc123
→ {
"currentProcess": "blocked",
"status": "open",
"info": { "reason": "provider_no_show" }
}
Get the recovery options:
GET /v1/workflow-instances/wf-abc123/work
→ {
"unblockedGoals": [
{ "publicId": "wf-abc123.blk.a1b2c3", "name": "reassign_provider" },
{ "publicId": "wf-abc123.blk.d4e5f6", "name": "reschedule" },
{ "publicId": "wf-abc123.blk.g7h8i9", "name": "escalate_to_landlord" }
],
"unblockedTasks": [...],
"tools": [...]
}
Complete the appropriate recovery goal — close its tasks, close the goal. The workflow returns to work_assigned with a new provider. The event log records the full path, including the blocked excursion. The workflow did not crash; the exception is a defined process with defined recovery.
The event log
Every decision — open a goal, close a task, advance to the next process — is recorded with timestamps, triggering actor, reason, and (for decisions made by the decision engine) a reasoning trace.
GET /v1/workflow-instances/wf-abc123/events
→ [
{ "type": "process_entered", "process": "vacated", ... },
{ "type": "goal_opened", "name": "document_condition", ... },
{ "type": "task_closed", "name": "photograph_rooms", "reason": "success", ... },
{ "type": "goal_closed", "name": "document_condition", "reason": "success", ... },
{ "type": "process_advanced", "from": "condition_documented", "to": "work_identified", ... },
...
]
The event log is the audit and replay artifact. It is also the input to operational metrics — conversion rates, dwell time per process, work-in-progress depth, error disposition — all derived from it for free.
Composability
Composable workflows reuse the same task primitives. The apartment.close_and_prep workflow dispatches tasks like:
inspect / apartment.unit [move_out_condition]— document the unit's state.clean / apartment.unit [turnover]— standard turn cleaning.verify / apartment.unit [work_completion]— confirm work was done.
Each task follows the same action / resource / subject / result grammar whether it runs standalone or inside a workflow. The workflow adds structure and evidence accumulation; it does not change how tasks work.
New workflows compose the same primitives in different sequences with different rules. A property.condition_check workflow reuses inspect tasks but with a lighter process model: requested → scheduled → on_site → documented → reported. The task vocabulary is shared; the workflow grammar is specific to the operation.
See also
- Decision Engine — how the platform makes decisions about which goals to pursue and which tasks to act on.
- Skills Authoring — how to write goal-and-judgment content for Smart Decisioning.
- Operational Metrics — what falls out of the event log for free.
- Agent Surface — the REST and MCP integration contract.
- Task Taxonomy — the action / resource / subject / result grammar that tasks compose from.