Pattern: Hypermedia-Driven Workflow

The Hypermedia-Driven Workflow pattern uses HAL-formatted (Hypertext Application Language) controls embedded directly in resource representations to guide clients dynamically through complex multi-step processes and state transitions.

Instead of hardcoding state machines, UI route paths, or action permission logic inside client applications, the API server drives application state (HATEOAS). By inspecting the hypermedia links (_links) served in each response, clients discover valid next actions, dynamically render appropriate pages or forms, and seamlessly support pausing and resuming workflows across devices and sessions.

23.1. Overview

In Pattern 22 (Draft Workflow Resource), we established how a dedicated resource instance stores state across a multi-step process before final execution. The Hypermedia-Driven Workflow pattern takes this concept a step further by removing the client’s need to assume what step or action comes next.

Rather than forcing front-end applications to maintain static state maps (e.g., if (status == 'DRAFT') showSubmitButton()), the API response explicitly instructs the client on what operations are possible right now by embedding Hypermedia links.

Note: Examples use HAL-based _links as they are the easiest to adopt into existing APIs as well as new APIs.

{
  "id": "art_9901",
  "title": "API Design Patterns for Enterprise REST",
  "status": "DRAFT",
  "_links": {
    "self": { "href": "/articles/art_9901" },
    "next-step": { "href": "/articles/art_9901/content-editor", "title": "Edit Content" },
    "submit": { "href": "/articles/art_9901/submit", "method": "POST", "title": "Submit for Review" }
  }
}

Pausing and Resuming Workflows

When a user pauses a workflow (e.g., closing a browser or switching devices), resuming is effortless:

  1. The client issues a GET request to retrieve the workflow resource instance.
  2. The client inspects the returned _links object and current status metadata.
  3. The client dynamically routes the user to the form or page referenced by the next-step or actionable link relations (e.g., edit-revisions or schedule).
  4. Only valid, permitted action buttons or form controls are rendered based on the presence of corresponding relation links (rel).

23.2. When to Use Hypermedia-Driven Workflow

Use Hypermedia-Driven Workflows when:

  • Workflows have dynamic or role-dependent state transitions.
    • e.g., A Content Management System (CMS) where authors see submit, editors see approve/request-revision, and publishers see schedule/publish.
  • Clients must pause and resume workflows at later dates.
    • Users can exit an application midway through a multi-step process and resume on a different client without losing context or requiring hardcoded client-side step tracking.
  • Workflow business rules change frequently.
    • Adding a new intermediate approval step (e.g., legal review before publishing) requires no client code updates; the server simply injects the new link relation into the response.
  • You want to decouple UI routing and permissions from the server state machine.
    • The client UI becomes a generic execution engine that renders screens based on link presence rather than embedded business logic.

23.3. When NOT to Use Hypermedia-Driven Workflow

Avoid Hypermedia-Driven Workflows when:

  • The workflow is strictly static and simple CRUD.
    • If a resource only supports standard GET, PUT, DELETE operations without business state transitions, HAL links add unnecessary payload overhead.
  • Building low-latency, machine-to-machine microservices.
    • Internal microservices that rely on compiled gRPC or tightly coupled SDK contracts may incur unnecessary CPU parsing overhead from hypermedia links.
  • Clients cannot parse dynamic link relations.
    • If client developers refuse to use link-based navigation and insist on hardcoding exact URI endpoints, hypermedia controls provide little value.

23.4. What the Pattern Looks Like

Below is a complete lifecycle traversal for a Content Management System (CMS) article resource transitioning through seven states: DRAFTSUBMITTEDREVISION_REQUESTEDAPPROVEDSCHEDULEDPUBLISHEDARCHIVED.


1. State: DRAFT (Pausing & Resuming)

An author retrieves a saved draft article. The server indicates that the author can continue editing (next-step) or submit for review.

Request

GET /articles/art_9901 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi... (Author Token)

Response

HTTP/1.1 200 OK
Content-Type: application/hal+json

{
  "id": "art_9901",
  "title": "API Design Patterns for Enterprise REST",
  "status": "DRAFT",
  "author": "Jane Doe",
  "_links": {
    "self": { "href": "/articles/art_9901" },
    "next-step": { "href": "/articles/art_9901/editor", "title": "Continue Editing Draft" },
    "submit": { "href": "/articles/art_9901/submit", "title": "Submit for Review" }
  }
}

2. State: SUBMITTED (Editor View)

The author calls the submit link. An editor fetches the article. Because the user is an editor and the status is SUBMITTED, the server exposes approve and request-revision links.

Request

GET /articles/art_9901 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi... (Editor Token)

Response

HTTP/1.1 200 OK
Content-Type: application/hal+json

{
  "id": "art_9901",
  "title": "API Design Patterns for Enterprise REST",
  "status": "SUBMITTED",
  "submittedAt": "2026-08-04T09:00:00Z",
  "_links": {
    "self": { "href": "/articles/art_9901" },
    "approve": { "href": "/articles/art_9901/approve", "title": "Approve Article" },
    "request-revision": { "href": "/articles/art_9901/request-revision", "title": "Request Revision" }
  }
}

3. State: REVISION_REQUESTED (Resumed by Author)

The editor requests revisions. When the author logs back in days later, the client fetches the article, reads status: REVISION_REQUESTED, and follows next-step (/articles/art_9901/revisions) to render the revision feedback form.

Request

GET /articles/art_9901 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi... (Author Token)

Response

HTTP/1.1 200 OK
Content-Type: application/hal+json

{
  "id": "art_9901",
  "title": "API Design Patterns for Enterprise REST",
  "status": "REVISION_REQUESTED",
  "revisionNotes": "Please add code examples for HAL links in Section 3.",
  "_links": {
    "self": { "href": "/articles/art_9901" },
    "next-step": { "href": "/articles/art_9901/revisions", "title": "Edit Requested Revisions" },
    "resubmit": { "href": "/articles/art_9901/submit", "title": "Resubmit for Review" }
  }
}

4. State: APPROVED & SCHEDULED (Publisher View)

Once approved, the publisher receives options to publish immediately or schedule publication.

Request

GET /articles/art_9901 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi... (Publisher Token)

Response

HTTP/1.1 200 OK
Content-Type: application/hal+json

{
  "id": "art_9901",
  "title": "API Design Patterns for Enterprise REST",
  "status": "APPROVED",
  "_links": {
    "self": { "href": "/articles/art_9901" },
    "publish": { "href": "/articles/art_9901/publish", "title": "Publish Now" },
    "schedule": { "href": "/articles/art_9901/schedule", "title": "Schedule Publication" }
  }
}

5. State: PUBLISHED & ARCHIVED

After the article goes live, editing links are removed. Only archive or unpublish actions remain.

Response (Published State)

HTTP/1.1 200 OK
Content-Type: application/hal+json

{
  "id": "art_9901",
  "title": "API Design Patterns for Enterprise REST",
  "status": "PUBLISHED",
  "publishedAt": "2026-08-04T12:00:00Z",
  "_links": {
    "self": { "href": "/articles/art_9901" },
    "canonical": { "href": "https://example.com/blog/api-design-patterns" },
    "archive": { "href": "/articles/art_9901/archive", "title": "Archive Article" }
  }
}

23.5. Anti-Patterns to Avoid

1. Hardcoding transition URLs inside client applications

  • Why it’s bad: Constructing transition URIs manually in JavaScript/mobile code (e.g., fetch('/articles/' + id + '/submit')) bypasses hypermedia, breaking client apps whenever server routing or business workflow rules evolve.
  • Returning an approve or publish link to an author who lacks editing permissions causes unnecessary 403 Forbidden errors when clicked. The server should dynamically filter _links based on the authenticated user’s permissions.

3. Storing workflow state in local browser storage

  • Relying on localStorage or cookies to remember where a user left off causes state synchronization bugs when users log in from a second device. Use the resource’s hypermedia state (GET /resources/{id}) as the single source of truth.
  • Inventing inconsistent link names (e.g., mixing nextStep, next_step, continue_url) makes UI client engine code fragile. Establish standardized link relation names across your domain API guidelines.

23.6. OpenAPI Example

A complete OpenAPI 3.0.3 specification illustrating how to document HAL _links schemas and dynamic state actions for a CMS article resource.

openapi: 3.0.3
info:
  title: CMS Article API - Hypermedia-Driven Workflow Example
  version: 1.0.0
servers:
  - url: https://api.example.com

paths:
  /articles/{articleId}:
    get:
      summary: Retrieve article with hypermedia workflow links
      description: Returns article state along with HAL '_links' indicating valid next workflow actions based on status and user roles.
      tags: [Articles]
      parameters:
        - in: path
          name: articleId
          required: true
          schema:
            type: string
            example: art_9901
      responses:
        '200':
          description: Article retrieved successfully with contextual hypermedia links
          content:
            application/hal+json:
              schema:
                $ref: '#/components/schemas/ArticleHAL'

  /articles/{articleId}/submit:
    post:
      summary: Submit article for review
      tags: [Article Workflow]
      parameters:
        - in: path
          name: articleId
          required: true
          schema:
            type: string
            example: art_9901
      responses:
        '200':
          description: Article submitted; returns updated state and HAL links
          content:
            application/hal+json:
              schema:
                $ref: '#/components/schemas/ArticleHAL'

  /articles/{articleId}/request-revision:
    post:
      summary: Request revisions from author
      tags: [Article Workflow]
      parameters:
        - in: path
          name: articleId
          required: true
          schema:
            type: string
            example: art_9901
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - revisionNotes
              properties:
                revisionNotes:
                  type: string
                  example: "Please add code examples for HAL links in Section 3."
      responses:
        '200':
          description: Revisions requested; returns updated HAL resource
          content:
            application/hal+json:
              schema:
                $ref: '#/components/schemas/ArticleHAL'

components:
  schemas:
    ArticleHAL:
      type: object
      properties:
        id:
          type: string
          example: art_9901
        title:
          type: string
          example: "API Design Patterns for Enterprise REST"
        status:
          type: string
          enum: [DRAFT, SUBMITTED, APPROVED, REVISION_REQUESTED, SCHEDULED, PUBLISHED, ARCHIVED]
          example: REVISION_REQUESTED
        author:
          type: string
          example: "Jane Doe"
        revisionNotes:
          type: string
          example: "Please add code examples for HAL links in Section 3."
        _links:
          type: object
          description: Contextual HAL hypermedia links driven by current resource state and permissions.
          additionalProperties:
            $ref: '#/components/schemas/HALLink'
          example:
            self:
              href: "/articles/art_9901"
            next-step:
              href: "/articles/art_9901/revisions"
              title: "Edit Requested Revisions"
            resubmit:
              href: "/articles/art_9901/submit"
              title: "Resubmit for Review"

    HALLink:
      type: object
      required:
        - href
      properties:
        href:
          type: string
          example: "/articles/art_9901/submit"
        title:
          type: string
          example: "Submit for Review"
        method:
          type: string
          example: "POST"

23.7. Visualizing Hypermedia-Driven Workflow (Mermaid Diagram)

sequenceDiagram
    autonumber
    actor Author as Author Client (UI)
    actor Editor as Editor Client (UI)
    participant API as API Server
    participant Workflow as Workflow Engine & State Machine

    Note over Author,Workflow: Phase 1: Author Pauses & Resumes Draft
    Author->>API: GET /articles/art_9901
    API->>Workflow: Check state & author permissions
    Workflow-->>API: Status: DRAFT
    API-->>Author: 200 OK (HAL Body with _links.next-step & _links.submit)
    
    Note over Author: Client evaluates _links.next-step & renders Content Editor UI
    Author->>API: POST /articles/art_9901/submit (Follows _links.submit)
    API->>Workflow: Transition status DRAFT -> SUBMITTED
    API-->>Author: 200 OK (Status: SUBMITTED, _links has self only)

    Note over Editor,Workflow: Phase 2: Editor Reviews & Requests Revision
    Editor->>API: GET /articles/art_9901
    API->>Workflow: Check state & editor permissions
    Workflow-->>API: Status: SUBMITTED
    API-->>Editor: 200 OK (HAL Body with _links.approve & _links.request-revision)
    
    Note over Editor: Client evaluates _links & renders Approve / Revision buttons
    Editor->>API: POST /articles/art_9901/request-revision { notes: "Add HAL code" }
    API->>Workflow: Transition status SUBMITTED -> REVISION_REQUESTED
    API-->>Editor: 200 OK (Status: REVISION_REQUESTED)

    Note over Author,Workflow: Phase 3: Author Resumes Workflow Later
    Author->>API: GET /articles/art_9901 (Days later from mobile app)
    API->>Workflow: Check state & author permissions
    Workflow-->>API: Status: REVISION_REQUESTED
    API-->>Author: 200 OK (HAL Body with _links.next-step: "/revisions")
    Note over Author: Client reads _links.next-step & dynamically opens Revision Form UI