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:

  • Goalswhat we want. Declared outcomes to be achieved within a process. Goals run concurrently by default; sequencing comes from dependsOn edges. A goal may be marked required — if a required goal closes with reason: failed, the workflow halts.
  • Taskswhat we'll do. Planned units of work that advance goals. Tasks may be added, deprecated, or replaced as the workflow runs. Tasks link via dependsOn edges (to other tasks or to goals), producing sequential or concurrent execution as the graph dictates.
  • Toolswhat 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 dependsOn edges are satisfied.
  • The goal can close only when its dependsOn edges are satisfied.
  • A task is unblocked only when both its own dependsOn edges and its parent goal's dependsOn edges 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:

  • statusopen | closed
  • reason — 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 required goal closes with reason: failed → the workflow closes with reason: failed.
  • If all goals in the current process are closed → advance to the next process (or close the workflow with reason: success if 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

ProcessWhat it meansTypical goals
vacatedLease ended, tenant departed.confirm_access, verify_keys_returned
access_confirmedA provider has verified they can enter.schedule_inspection
condition_documentedThe unit's state has been recorded.document_condition, assess_severity
work_identifiedSpecific work items have been scoped.scope_cleaning, scope_repairs, scope_paint
work_assignedProviders routed for each work item.dispatch_cleaner, dispatch_repair, dispatch_paint
work_completedAll dispatched work reports completion.verify_cleaning, verify_repair, verify_paint
verifiedA verification visit confirms work.final_walkthrough, photograph_completed
readyConfirmed 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.