REST API#

This package extends plone.restapi in three places: the @workflow and @history endpoints, and the serialization of content itself. Almost every change is an addition, and every key the API already returned keeps its value, with one exception: the top-level transitions list of @workflow on participating content, described under Compatibility.

The additions to @workflow and @history apply to participating content only. Content that provides no participating behavior is served exactly the payload core produces there, as shown under Content without additional workflows.

The workflow_states key of the content serialization is the exception. It is added to every Dexterity object, participating or not, because every object with a workflow has a value for it.

See also

The endpoints these extend are documented in the Plone REST API reference, under Workflow and History. Content serialization and summaries are described under Serialization and Search.

GET @workflow#

Returns the workflow information for a content object.

http

GET /plone/member-profile/@workflow HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/member-profile/@workflow -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/member-profile/@workflow Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/member-profile/@workflow', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@id": "http://localhost:55001/plone/member-profile/@workflow",
    "chain": [
        {
            "history": [
                {
                    "action": null,
                    "actor": "admin",
                    "comments": "",
                    "review_state": "private",
                    "time": "1995-07-31T17:30:00+00:00"
                }
            ],
            "state": {
                "id": "private",
                "title": "Private"
            },
            "state_variable": "review_state",
            "title": "Simple Publication Workflow",
            "transitions": [
                {
                    "@id": "http://localhost:55001/plone/member-profile/@workflow/publish",
                    "title": "Publish"
                },
                {
                    "@id": "http://localhost:55001/plone/member-profile/@workflow/submit",
                    "title": "Submit for publication"
                }
            ],
            "workflow_id": "simple_publication_workflow"
        },
        {
            "history": [
                {
                    "action": null,
                    "actor": "admin",
                    "comments": "",
                    "time": "1995-07-31T17:30:00+00:00",
                    "workflow_states": "pending"
                }
            ],
            "state": {
                "id": "pending",
                "title": "Pending"
            },
            "state_variable": "workflow_states",
            "title": "Membership",
            "transitions": [
                {
                    "@id": "http://localhost:55001/plone/member-profile/@workflow/activate",
                    "title": "Activate"
                }
            ],
            "workflow_id": "foundation_member_workflow"
        }
    ],
    "history": [
        {
            "action": null,
            "actor": "admin",
            "comments": "",
            "review_state": "private",
            "time": "1995-07-31T17:30:00+00:00",
            "title": "Private"
        }
    ],
    "state": {
        "id": "private",
        "title": "Private"
    },
    "transitions": [
        {
            "@id": "http://localhost:55001/plone/member-profile/@workflow/publish",
            "title": "Publish"
        },
        {
            "@id": "http://localhost:55001/plone/member-profile/@workflow/submit",
            "title": "Submit for publication"
        }
    ]
}

The chain key#

chain holds one entry per workflow applying to the object, in chain order. The first entry is always the workflow configured for the content type—the one driving review_state. Contributed workflows follow, in the order Order of contributions describes.

Each entry holds the following keys.

workflow_id

Id of the workflow, as portal_workflow registers it.

title

The name to show for the workflow, translated: the label declared on its <plone:additionalworkflows /> directive, or the workflow's own title when none is declared. See ZCML directive.

state_variable

The variable this workflow drives. review_state for the primary workflow; never review_state for an additional one.

state

The object's current state in this workflow, as an object with id and a translated title. The title comes from the workflow's own state definition, so it resolves for a contributed workflow that the content type's configured chain does not mention.

transitions

The transitions of this workflow available to the current user, each with an @id to POST to and a translated title. A transition id defined by more than one workflow in the chain is listed under the first workflow that defines it. The workflow tool executes such an id in the first workflow that can execute it from its current state, so the entry a transition is listed under can differ from the workflow a POST to its @id moves. Give transitions ids that no other workflow in the chain uses.

history

This workflow's own review_history entries. A workflow that records no history reports an empty list.

The top-level transitions key#

For participating content, the top-level transitions list is narrowed to the primary workflow's transitions.

This is deliberate. A client written before this package existed reads that list and offers its contents as publication actions; widening it would make an unrelated workflow's transitions appear in a publication menu. Every transition remains available under chain, grouped by the workflow that owns it.

Content without additional workflows#

A content object providing no participating behavior is served core's payload, with no chain key.

http

GET /plone/plain-doc/@workflow HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/plain-doc/@workflow -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/plain-doc/@workflow Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/plain-doc/@workflow', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@id": "http://localhost:55001/plone/plain-doc/@workflow",
    "history": [
        {
            "action": null,
            "actor": "admin",
            "comments": "",
            "review_state": "private",
            "time": "1995-07-31T17:30:00+00:00",
            "title": "Private"
        }
    ],
    "state": {
        "id": "private",
        "title": "Private"
    },
    "transitions": [
        {
            "@id": "http://localhost:55001/plone/plain-doc/@workflow/publish",
            "title": "Publish"
        },
        {
            "@id": "http://localhost:55001/plone/plain-doc/@workflow/submit",
            "title": "Submit for publication"
        }
    ]
}

Important

Test for the presence of the chain key rather than assuming it. A client that reads chain unconditionally breaks on the first non-participating object it meets.

POST @workflow/{transition}#

Executes a transition. This endpoint is core's, unchanged: a transition belonging to an additional workflow is executed exactly like a publication transition, using the @id reported for it under chain.

http

POST /plone/member-profile/@workflow/activate HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X POST http://nohost/plone/member-profile/@workflow/activate -H "Accept: application/json" --user admin:secret

httpie

http POST http://nohost/plone/member-profile/@workflow/activate Accept:application/json -a admin:secret

python-requests

requests.post('http://nohost/plone/member-profile/@workflow/activate', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "action": null,
    "actor": "admin",
    "comments": "",
    "review_state": "private",
    "time": "1995-07-31T17:30:00+00:00",
    "title": "Private"
}

Note

The examples on this page use the example membership workflow the demo package ships, whose transition ids are activate and lapse. Those bare ids predate the advice to prefix transition ids with something specific to their workflow, such as membership_activate, which How to write a workflow that composes gives. Prefer prefixed ids in a workflow of your own.

The response reports review_state, as it always has. A transition belonging to an additional workflow does not change it—read the new state from chain with a follow-up GET, or from the @workflow expansion of the object.

GET @history#

Returns the history of every workflow in the chain, merged into one stream, newest first.

http

GET /plone/member-profile/@history HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/member-profile/@history -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/member-profile/@history Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/member-profile/@history', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

[
    {
        "action": "activate",
        "actor": {
            "@id": "http://localhost:55001/plone/@users/admin",
            "fullname": "admin",
            "id": "admin",
            "username": null
        },
        "comments": "",
        "state_title": "Active",
        "time": "1995-07-31T17:30:00+00:00",
        "transition_title": "Activate",
        "type": "workflow",
        "workflow_id": "foundation_member_workflow",
        "workflow_states": "active"
    },
    {
        "@id": "http://localhost:55001/plone/member-profile/@history/0",
        "action": "Edited",
        "actor": {
            "@id": "http://localhost:55001/plone/@users/admin",
            "fullname": "admin",
            "id": "admin",
            "username": null
        },
        "comments": "Initial version",
        "may_revert": true,
        "time": "1995-07-31T18:30:00+00:00",
        "transition_title": "Edited",
        "type": "versioning",
        "version": 0,
        "workflow_id": null
    },
    {
        "action": "Create",
        "actor": {
            "@id": "http://localhost:55001/plone/@users/admin",
            "fullname": "admin",
            "id": "admin",
            "username": null
        },
        "comments": "",
        "state_title": "Pending",
        "time": "1995-07-31T19:30:00+00:00",
        "transition_title": "Create",
        "type": "workflow",
        "workflow_id": "foundation_member_workflow",
        "workflow_states": "pending"
    },
    {
        "action": "Create",
        "actor": {
            "@id": "http://localhost:55001/plone/@users/admin",
            "fullname": "admin",
            "id": "admin",
            "username": null
        },
        "comments": "",
        "review_state": "private",
        "state_title": "Private",
        "time": "1995-07-31T20:30:00+00:00",
        "transition_title": "Create",
        "type": "workflow",
        "workflow_id": "simple_publication_workflow"
    }
]

The workflow_id key#

Every entry carries a workflow_id, including the ones core produces.

  • For a workflow entry, it is the id of the workflow that recorded the transition.

  • For a versioning entry, it is null. Such an entry belongs to no workflow.

The key is present on every entry, so a client may read it without a guard.

Entries are otherwise shaped exactly as core shapes them, and carry the same keys. A workflow entry also carries its own workflow's state variable: review_state for the primary workflow, and whatever variable an additional workflow declares.

Note

Each workflow's review_history is read separately, so each workflow's own info guard decides whether its entries are visible to the current user. An entry the guard hides is absent rather than redacted.

Content serialization#

The serialization of a content object—what GET on its URL returns—gains a workflow_states key.

Important

The key is added by patching plone.restapi. When this package's ZCML is loaded, the __call__ method of plone.restapi's SerializeToJson class is wrapped in place. Its folder and collection serializers, and any serializer an add-on derives from them, reach that method through super(), so they carry the key too. A serializer overriding __call__ without calling super() does not, and neither does the Plone site root, which has a serializer of its own.

http

GET /plone/member-profile HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET http://nohost/plone/member-profile -H "Accept: application/json" --user admin:secret

httpie

http http://nohost/plone/member-profile Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/member-profile', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@components": {
        "actions": {
            "@id": "http://localhost:55001/plone/member-profile/@actions"
        },
        "aliases": {
            "@id": "http://localhost:55001/plone/member-profile/@aliases"
        },
        "breadcrumbs": {
            "@id": "http://localhost:55001/plone/member-profile/@breadcrumbs"
        },
        "contextnavigation": {
            "@id": "http://localhost:55001/plone/member-profile/@contextnavigation"
        },
        "navigation": {
            "@id": "http://localhost:55001/plone/member-profile/@navigation"
        },
        "navroot": {
            "@id": "http://localhost:55001/plone/member-profile/@navroot"
        },
        "types": {
            "@id": "http://localhost:55001/plone/member-profile/@types"
        },
        "workflow": {
            "@id": "http://localhost:55001/plone/member-profile/@workflow"
        }
    },
    "@id": "http://localhost:55001/plone/member-profile",
    "@type": "Profile",
    "UID": "55c25ebc220d400393574f37d648727c",
    "allow_discussion": false,
    "changeNote": "",
    "contributors": [],
    "created": "1995-07-31T13:45:00+00:00",
    "creators": [
        "admin"
    ],
    "description": "",
    "email": null,
    "id": "member-profile",
    "image": null,
    "image_caption": null,
    "is_folderish": true,
    "items": [],
    "items_total": 0,
    "layout": "view",
    "lock": {},
    "modified": "1995-07-31T17:30:00+00:00",
    "next_item": {},
    "parent": {
        "@id": "http://localhost:55001/plone",
        "@type": "Plone Site",
        "description": "",
        "title": "Plone site",
        "type_title": "Plone Site"
    },
    "previous_item": {},
    "review_state": "private",
    "rights": "",
    "title": "A Member Profile",
    "type_title": "Profile",
    "version": "current",
    "versioning_enabled": true,
    "workflow_states": [
        "simple_publication_workflow|private",
        "foundation_member_workflow|pending"
    ],
    "working_copy": null,
    "working_copy_of": null
}

The workflow_states key#

workflow_states holds the object's state in every workflow of its chain, one <workflow-id>|<state-id> value per workflow, in chain order. These are exactly the values the workflow_states catalog index holds, as described in Catalog index.

  • The first value is always the workflow driving review_state, and it agrees with the review_state key of the same payload.

  • The key is present on every Dexterity object. An object with no workflow at all reports an empty list.

  • For a historical version, requested through GET @history/{version}, the states are read from that version, which is where review_state is read from too.

Summaries of catalog results#

Summaries built from catalog results carry workflow_states as well, read from the metadata column of the same name. That covers the items of a folder and the results of @search and @querystring-search.

http

GET /plone/@search?portal_type=Profile HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0

curl

curl -i -X GET 'http://nohost/plone/@search?portal_type=Profile' -H "Accept: application/json" --user admin:secret

httpie

http 'http://nohost/plone/@search?portal_type=Profile' Accept:application/json -a admin:secret

python-requests

requests.get('http://nohost/plone/@search?portal_type=Profile', headers={'Accept': 'application/json'}, auth=('admin', 'secret'))
HTTP/1.1 200 OK
Content-Type: application/json

{
    "@id": "http://localhost:55001/plone/@search?portal_type=Profile",
    "items": [
        {
            "@id": "http://localhost:55001/plone/member-profile",
            "@type": "Profile",
            "description": "",
            "effective": "1995-07-31T17:30:00+00:00",
            "end": null,
            "getObjSize": "0 KB",
            "getRemoteUrl": null,
            "head_title": null,
            "image_field": "",
            "image_scales": null,
            "mime_type": "text/plain",
            "nav_title": null,
            "review_state": "private",
            "start": null,
            "title": "A Member Profile",
            "type_title": "Profile",
            "workflow_states": [
                "simple_publication_workflow|private",
                "foundation_member_workflow|pending"
            ]
        }
    ],
    "items_total": 1
}

A client can therefore show each item's state in every workflow without one request per item.

Compatibility#

The tables below compare each payload with what plone.restapi returns without this package.

Unchanged#

Payload

Behavior

@workflow on non-participating content

core's payload

@workflow keys other than transitions

core's values

POST @workflow/{transition}

core's behavior

Added#

Payload

Addition

@workflow on participating content

a chain key

@history entries on participating content

a workflow_id key

Content serialization

a workflow_states key

Summaries of catalog results

a workflow_states field

Changed#

Payload

Without this package

With this package

@workflow transitions on participating content

every transition the chain offers

the primary workflow's transitions only

A client that relies on the top-level transitions list to reach every workflow's transitions must read them from chain instead. Only a site whose chains already held several workflows before this package was installed can have such a client, because on a chain of one workflow the two lists hold the same transitions.

The examples on this page are generated by the test suite, in backend/tests/docs/, and are regenerated on every run. A payload that changes shape without the documentation changing with it fails the suite.