How to write a workflow that composes#

This guide shows you how to write a workflow definition that can be appended to a chain without disturbing the workflow already there.

Four constraints. Satisfy them and your workflow composes with any other that satisfies them too.

Prerequisites#

  • A GenericSetup profile that ships a workflow definition, in profiles/<name>/workflows/<workflow-id>/definition.xml.

1. Declare workflow_states as the state variable#

<dc-workflow
    workflow_id="foundation_member_workflow"
    state_variable="workflow_states"
    title="Membership"
    >

Never review_state. That variable belongs to the workflow your content type is configured with, and claiming it makes the answer to an unqualified state question depend on chain order.

Use workflow_states rather than a name of your own. WorkflowTool._reindexWorkflowVariables reindexes the catalog indexes named after the chain's workflow variables, so this name keeps the workflow_states index fresh on every transition with no subscriber involved.

A bespoke name still works—an event handler covers it—but it costs a full metadata rebuild on each transition that the shared name avoids.

2. Give every transition a unique id#

Transition ids must be unique across every workflow in the chains your workflow will join.

<transition transition_id="activate"
            new_state="active"
            title="Activate membership"
            trigger="USER"
            />

doActionFor resolves an ambiguous id to the first workflow in the chain that defines it. A transition named publish in your workflow is therefore unreachable behind the publication workflow's own publish, except by naming your workflow explicitly.

Prefixing ids with something specific to your domain, such as membership_activate rather than activate, is the cheapest way to guarantee this in a site whose chains you do not control.

3. Declare only permissions nothing else manages#

If your workflow controls access, list the permissions it manages and keep that list disjoint from every other workflow in the chain.

<permission>my.package: Manage membership</permission>

Declaring a permission that another workflow in the chain also declares leaves the object's mapping as whichever workflow transitioned last, silently, until the other transitions or updateRoleMappings() runs.

The three permissions Plone's publication workflow manages are the ones to avoid by default. Defining a permission of your own, as above, is almost always the right move.

4. Verify the composition#

Create an object of the participating type and audit its chain.

from collective.multiworkflow import api as mw_api

assert mw_api.conflicting_permissions(obj) == {}

An empty mapping means no permission in the chain has more than one claimant. Anything else names the permission and the workflows fighting over it.

Then check the transition ids resolve to the workflows you expect.

owners = mw_api.owning_workflow(obj)

assert owners["activate"] == "foundation_member_workflow"

A transition id attributed to a workflow you did not expect is a shadowing collision. Rename it and reapply your profile.

See also

About state variables and review_state and About permissions in a chain for why these constraints exist.