Workflows: unopinionated substrate (Spec, Runner, abilities, AS bridge)#114
Merged
Conversation
Adds an unopinionated workflow plumbing layer in the spirit of the
existing Channels and Memory primitives — the substrate ships contracts,
value objects, validators, an in-memory registry, an optional Action
Scheduler bridge, and three canonical Abilities API entry points; every
consumer brings its own storage and run-recorder implementations.
Why this lands here. The two existing references — James LePage's
six-month-old design spike (lepagejames/workflows-api, design-only,
no implementation) and the HAL team's wpcom workflow engine (production
since Aug 2025, closed-source inside wpcom) — converge on the same
shape: deterministic orchestration over registered abilities and agents,
with the runtime owned by whichever consumer wants to drive it. Data
Machine already runs its own pipelines/flows/jobs against custom tables;
openclaWP wants a smaller CPT-backed runtime; future consumers may want
neither. Agents-api is the right place for the contract those runtimes
share.
What ships:
- WP_Agent_Workflow_Spec / Spec_Validator — immutable value object plus
a structural validator that returns `[ path, code, message ]` triples
so editor / REST surfaces can show inline lints. Validator rejects
missing ids, empty step lists, duplicate step ids, unknown step or
trigger types (with filter extensibility for `branch` / `parallel` /
`workflow` etc. to land later).
- WP_Agent_Workflow_Bindings — pure helper that expands `${inputs.x}`
and `${steps.<id>.output.<path>}` template tokens against a
resolution context. Whole-string templates preserve native types;
mixed-content templates stringify with type coercion; missing paths
return null in atomic mode and empty string in mixed mode. Numeric
path segments index lists.
- WP_Agent_Workflow_Run_Result — immutable execution outcome with
pending / running / succeeded / failed / skipped statuses, per-step
records, and a `with()` patch helper.
- WP_Agent_Workflow_Store / Run_Recorder — interfaces only; no
default implementations. find / save / delete / all and start /
update / find / recent. Recorder failures are best-effort tolerated
so persistence outages don't break user-facing runs.
- WP_Agent_Workflow_Runner — abstract execution engine. Walks steps in
order, dispatches via a step-type handler map (`ability` and `agent`
ship; consumers add `branch`, `workflow`, `parallel` via the
`wp_agent_workflow_step_handlers` filter), records per-step state,
short-circuits on failure unless `continue_on_error` is set, and
validates required inputs upfront. The default `agent` handler routes
through the canonical `agents/chat` dispatcher (per #100) so every
workflow benefits from registered chat runtimes automatically.
- WP_Agent_Workflow_Registry — in-memory registration via
`wp_register_workflow()` / `wp_get_workflow()`. Purely volatile;
consumers pair with a Store for durable persistence.
- WP_Agent_Workflow_Action_Scheduler_Bridge — optional bridge that
no-ops cleanly when AS isn't loaded. Fires
`wp_agent_workflow_schedule_requested` regardless so custom
schedulers can take over. Workflows without `cron` triggers don't
require AS at all.
- register-agents-workflow-abilities.php — three canonical abilities
(agents/run-workflow, agents/validate-workflow,
agents/describe-workflow) under the existing `agents-api` category.
Run-workflow follows the agents/chat dispatcher pattern: filter
`wp_agent_workflow_handler` resolves a runtime, observability hook
`agents_run_workflow_dispatch_failed` fires on every non-success
exit. Convenience helper `register_workflow_handler()` mirrors the
chat one.
Tests: four smoke files covering bindings (13 assertions), spec +
validator (20), runner (17), and ability dispatchers (18). All-green
locally; the existing 950+ assertions continue to pass.
Addresses the four blocking issues from the PR review pass:
1. AS bridge `register()` no longer resets the recurring-action clock on
every plugin boot. `is_available()` now also requires
`as_next_scheduled_action`, and `register()` skips re-scheduling when
an action with matching hook + args is already in the queue. Without
this guard, short-interval workflows on a busy site never fire — every
request was unscheduling and re-scheduling, pushing next-run forward.
2. Runner now treats `recorder->start()` returning `WP_Error` as a
first-class failure (`recorder_start_failed`). Step pipeline does not
run; caller still gets a result so observability hooks fire.
3. Input-validation failure goes through the proper `start → update`
recorder lifecycle. Previously the early-exit path called `start()`
with an already-FAILED result, which violates the contract recorders
reasonably expect (start = pending/running, update = transitions).
Added a lifecycle smoke that pins the order.
4. README boundary section reshaped: `Concrete workflow runtimes,
durable workflow / run history, scheduling adapters beyond the
optional Action Scheduler bridge, workflow editor UI, and
product-specific step types` instead of the old "no workflows at
all" line. Added the workflow-related entries to "What Agents API
Owns" and the Public Surface list.
Plus the non-blocking review items worth shipping in the same PR:
- `agents/validate-workflow` is gated by a separate
`agents_validate_workflow_permission` callback (default
`is_user_logged_in()`) so non-admin authors can lint specs they're
writing. `agents/run-workflow` and `agents/describe-workflow` keep the
stricter `manage_options` default.
- Validator now catches forward / unknown step-id binding references at
spec-construction time, so a typo in `${steps.frist.output.x}`
surfaces a structured error instead of resolving to null at runtime.
- `Registry::unregister()` now returns `true|WP_Error` to match the
shape of the `Store::delete()` contract.
- Documented `output.last` semantics on the runner: present only when
the last step succeeded; with `continue_on_error` the convenience
shortcut may be absent and callers should use
`$result->get_output()['steps'][<id>]` for partial-success lookups.
CI lint compliance:
- Split the in-memory registry's global helpers into
`src/Workflows/register-workflows.php` so `class-wp-agent-workflow-registry.php`
is a pure class file (Universal.Files.SeparateFunctionsFromOO).
- Replaced `array_filter( …, 'strlen' )` in
`WP_Agent_Workflow_Bindings::resolve_path()` with an explicit closure
to satisfy phpstan level 7's strict callable check.
- Tightened `validate_steps()` / `validate_triggers()` return types from
`array<int,array>` to the precise `array{path:string,code:string,message:string}`
shape so phpstan doesn't widen the public `validate()` return.
- Reworded an inline comment that PHPCS flagged as 47% commented-out
code.
- Reformatted the small associative array in `agents_describe_workflow()`
to one entry per line so PHPCS's `WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound`
no longer trips.
Tests: smokes now total 81 assertions across the four files (+13 over
the previous pass) covering forward-ref + filter-extended trigger types
in the validator and the new recorder-error / lifecycle paths in the
runner. `composer test` is fully green locally.
This was referenced May 8, 2026
lezama
added a commit
that referenced
this pull request
May 9, 2026
…scheduled` The Action Scheduler bridge has shipped since #114, but only the half that *registers* schedules. When AS fires the recurring action under `wp_agent_workflow_run_scheduled`, nothing was listening — so cron- triggered workflows queued correctly and then never ran. This adds the missing handler. On every fired action it: - Reads the `workflow_id` from the scheduled args. - Looks up the canonical `agents/run-workflow` ability. - Temporarily widens `agents_run_workflow_permission` (AS runs as the loopback / cron user, which doesn't have manage_options). - Dispatches through the same code path admins / channels use for on-demand runs, so the runtime registered by the consumer plugin handles persistence + execution exactly as it does for ad-hoc runs. Failures fire `agents_run_workflow_dispatch_failed` with a structured reason (`no_workflow_id`, `abilities_api_missing`, `ability_missing`, or the dispatcher's own error code) and otherwise stay quiet — throwing out of an AS callback would mark the action as failed and exponentially back-off the schedule, which is rarely what you want when the failure is "the consumer plugin is mid-deploy." composer.json picks up `woocommerce/action-scheduler` as a `suggest`. The substrate stays runtime-detect: the bridge already no-ops cleanly when AS isn't loaded, so the loaded-anywhere case (WooCommerce site, dedicated AS install, openclaWP's bundled copy) gets cron for free.
lezama
added a commit
that referenced
this pull request
May 9, 2026
…scheduled` (#121) The Action Scheduler bridge has shipped since #114, but only the half that *registers* schedules. When AS fires the recurring action under `wp_agent_workflow_run_scheduled`, nothing was listening — so cron- triggered workflows queued correctly and then never ran. This adds the missing handler. On every fired action it: - Reads the `workflow_id` from the scheduled args. - Looks up the canonical `agents/run-workflow` ability. - Temporarily widens `agents_run_workflow_permission` (AS runs as the loopback / cron user, which doesn't have manage_options). - Dispatches through the same code path admins / channels use for on-demand runs, so the runtime registered by the consumer plugin handles persistence + execution exactly as it does for ad-hoc runs. Failures fire `agents_run_workflow_dispatch_failed` with a structured reason (`no_workflow_id`, `abilities_api_missing`, `ability_missing`, or the dispatcher's own error code) and otherwise stay quiet — throwing out of an AS callback would mark the action as failed and exponentially back-off the schedule, which is rarely what you want when the failure is "the consumer plugin is mid-deploy." composer.json picks up `woocommerce/action-scheduler` as a `suggest`. The substrate stays runtime-detect: the bridge already no-ops cleanly when AS isn't loaded, so the loaded-anywhere case (WooCommerce site, dedicated AS install, openclaWP's bundled copy) gets cron for free.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an unopinionated workflow plumbing layer in the spirit of Channels and Memory — substrate ships contracts, value objects, validators, an in-memory registry, an optional Action Scheduler bridge, and three canonical Abilities API entry points; every consumer brings its own storage and run-recorder implementations.
The two existing references — James LePage's six-month-old design spike (design-only, no implementation) and the HAL team's wpcom workflow engine (production since Aug 2025, closed-source inside wpcom) — converge on the same shape: deterministic orchestration over registered abilities and agents, with the runtime owned by whichever consumer wants to drive it. Data Machine already runs its own pipelines/flows/jobs against custom tables; openclaWP wants a smaller CPT-backed runtime; future consumers may want neither. agents-api is the right place for the contract those runtimes share.
Per Chris Huber on 2026-05-08: "we will handle it similar to memory, where we provide unopinionated plumbing."
What ships
WP_Agent_Workflow_Spec+Spec_Validator[ path, code, message ]triples for editor / REST surfacesWP_Agent_Workflow_Bindings${inputs.x}and${steps.<id>.output.<path>}templates. Whole-string templates preserve native types; mixed-content templates stringify; missing paths return null (atomic) / empty string (mixed)WP_Agent_Workflow_Run_Resultpending/running/succeeded/failed/skippedstatusesWP_Agent_Workflow_StoreWP_Agent_Workflow_Run_RecorderWP_Agent_Workflow_Runnerabilityandagentship;branch/parallel/workflowextensible via filter), per-step recording, short-circuit on failure unlesscontinue_on_erroris set. Defaultagenthandler routes through the canonicalagents/chatdispatcher (per #100)WP_Agent_Workflow_Registrywp_register_workflow()/wp_get_workflow()— pair with a Store for durable persistenceWP_Agent_Workflow_Action_Scheduler_Bridgewp_agent_workflow_schedule_requestedregardless so custom schedulers can take overregister-agents-workflow-abilities.phpagents-apicategory:agents/run-workflow(dispatcher mirroringagents/chat),agents/validate-workflow(pure substrate, no runtime),agents/describe-workflowSample spec
Out of scope (deferred to consumers / v1)
wp_agent_workflow_step_handlersfilter, no built-ins shiptype: workflow) — same extension hookTests
Adds four smoke files under
tests/:workflow-bindings-smoke.php— 13 assertions, template substitution edge casesworkflow-spec-validator-smoke.php— 20 assertions, structural validation + Spec value objectworkflow-runner-smoke.php— 17 assertions, full execute path with stub abilities + recorderagents-workflow-ability-smoke.php— 18 assertions, dispatcher pattern (mirrorsagents-chat-ability-smoke)All 950+ existing assertions continue to pass; bootstrap-smoke's
expected_source_directoriesallowlist gainsWorkflows/.Sequencing
A consumer PR to
lezama/openclawpfollows this one — CPT-backed Store + Run_Recorder, wp-admin UI, and a demo workflow shipped behind the existing example-agent opt-in filter pattern. Coordinated separately with Chris on Data Machine adoption — non-blocking for either consumer.Test plan
composer install && composer test— every smoke greenwp eval 'wp_register_workflow([ ... ]); print_r( wp_get_workflow( "demo/x" )->to_array() );'to confirm in-memory registry round-tripwp eval 'wp_get_ability( "agents/validate-workflow" )->execute([ "spec" => [ ... ] ])'to confirm the validate ability lands