Skip to content

Workflows: unopinionated substrate (Spec, Runner, abilities, AS bridge)#114

Merged
lezama merged 3 commits into
mainfrom
add/workflows-substrate
May 8, 2026
Merged

Workflows: unopinionated substrate (Spec, Runner, abilities, AS bridge)#114
lezama merged 3 commits into
mainfrom
add/workflows-substrate

Conversation

@lezama
Copy link
Copy Markdown
Contributor

@lezama lezama commented May 8, 2026

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

Component Purpose
WP_Agent_Workflow_Spec + Spec_Validator Immutable value object + structural validator returning [ path, code, message ] triples for editor / REST surfaces
WP_Agent_Workflow_Bindings Pure helper that expands ${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_Result Immutable execution outcome with pending / running / succeeded / failed / skipped statuses
WP_Agent_Workflow_Store Interface only — no default impl. Consumers wire their preferred persistence
WP_Agent_Workflow_Run_Recorder Interface only — same model
WP_Agent_Workflow_Runner Abstract execution engine. Sequential steps, step-type handler map (ability and agent ship; branch / parallel / workflow extensible via filter), per-step recording, short-circuit on failure unless continue_on_error is set. Default agent handler routes through the canonical agents/chat dispatcher (per #100)
WP_Agent_Workflow_Registry In-memory wp_register_workflow() / wp_get_workflow() — pair with a Store for durable persistence
WP_Agent_Workflow_Action_Scheduler_Bridge Optional bridge — no-ops cleanly when AS isn't loaded. Fires wp_agent_workflow_schedule_requested regardless so custom schedulers can take over
register-agents-workflow-abilities.php Three abilities under the existing agents-api category: agents/run-workflow (dispatcher mirroring agents/chat), agents/validate-workflow (pure substrate, no runtime), agents/describe-workflow

Sample spec

return array(
    'id'      => 'triage-inbound-customer-request',
    'version' => '1.0.0',
    'inputs'  => array( 'request' => array( 'type' => 'string', 'required' => true ) ),
    'steps'   => array(
        array(
            'id'      => 'classify',
            'type'    => 'agent',
            'agent'   => 'ops-triage',
            'message' => 'Classify and decide next action: ${inputs.request}',
        ),
        array(
            'id'      => 'create-ticket',
            'type'    => 'ability',
            'ability' => 'linear/create-issue',
            'args'    => array(
                'title' => '${steps.classify.output.title}',
                'body'  => '${steps.classify.output.summary}',
            ),
        ),
    ),
    'triggers' => array(
        array( 'type' => 'on_demand' ),
        array( 'type' => 'wp_action', 'hook' => 'comment_post' ),
    ),
);

Out of scope (deferred to consumers / v1)

  • Branching / conditionals / parallel steps — extensible today via wp_agent_workflow_step_handlers filter, no built-ins ship
  • Nested workflows (type: workflow) — same extension hook
  • YAML DSL — companion plugin; PHP array stays canonical
  • Pause / resume / approval gates
  • Workflow editor UI
  • Durable storage / run history — every consumer ships its own (Data Machine keeps its tables; openclaWP will add CPTs in a follow-up PR)

Tests

composer test

Adds four smoke files under tests/:

  • workflow-bindings-smoke.php — 13 assertions, template substitution edge cases
  • workflow-spec-validator-smoke.php — 20 assertions, structural validation + Spec value object
  • workflow-runner-smoke.php — 17 assertions, full execute path with stub abilities + recorder
  • agents-workflow-ability-smoke.php — 18 assertions, dispatcher pattern (mirrors agents-chat-ability-smoke)

All 950+ existing assertions continue to pass; bootstrap-smoke's expected_source_directories allowlist gains Workflows/.

Sequencing

A consumer PR to lezama/openclawp follows 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 green
  • In a real WP, wp eval 'wp_register_workflow([ ... ]); print_r( wp_get_workflow( "demo/x" )->to_array() );' to confirm in-memory registry round-trip
  • In a real WP, wp eval 'wp_get_ability( "agents/validate-workflow" )->execute([ "spec" => [ ... ] ])' to confirm the validate ability lands

lezama added 3 commits May 8, 2026 12:25
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.
@lezama lezama merged commit a884e69 into main May 8, 2026
2 checks passed
@lezama lezama deleted the add/workflows-substrate branch May 8, 2026 15:56
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant