Scrydon
Authoring: Process Flows

Rejection and Rework Loops

How to author backward sequence flows — approval reject arms and manual send-back — and how dependency-driven re-open protects completed work while redoing exactly what needs redoing.

How to think about it. A Process Flow template is a sequence of phases (Stages). Forward progression follows the normal BPMN sequence flow. Branching and parallelism belong inside a Stage — author them as a canvas workflow attached to a workflow action (the router block for exclusive gateways, parallel sub-processes for fan-out). Rejection and rework happen between Stages: when an approval gate rejects, or a reviewer sends a card back on the Kanban board, the platform routes execution backward to an earlier Stage — the BPMN rework-loop pattern. The three-level model (Stage / Task / Action) echoes CMMN plan-item terminology, but the execution model is BPMN: directed sequence flow with event-driven backward arcs.

Three ways a process goes back

1. Approval reject arm

An approval action can declare what happens when the approver clicks Reject. Set onReject.returnToStage on the action's metadata to the slug of an earlier Stage. When that approver rejects the gate, the platform routes the instance back to that Stage automatically.

import { defineAction } from '@scrydon/sdk-authoring/process-flows'

defineAction({
  name: 'Approve rollout plan',
  actionType: 'approval',
  isRequired: true,
  executionMode: 'manual',
  metadata: {
    persona: 'champion',
    onReject: {
      returnToStage: 'planning', // ← slug of the earlier stage to reopen
    },
  },
})

The slug must resolve to a Stage that is earlier in the template's stage sequence. The platform validates this at pack import — a reject arm pointing forward or to the same stage is rejected with a validation error.

When rejection fires:

  1. The approval gate records the rejection (actor, timestamp, any comment).
  2. The instance transitions backward to the returnToStage.
  3. The platform applies the dependency-driven re-open rules described below to decide which downstream work is re-activated and which is left intact.

Rejection does not delete history. The activity log retains the prior run's completed events — documents, uploads, approvals — even for re-opened actions. Reviewers see the full timeline of what changed between rounds.

2. Manual send-back

On the Kanban board, a workspace admin or flow owner can drag a card back to an earlier completed Stage column, or use the card's Send back to… menu. This is the manual equivalent of the approval reject arm.

Before the move is committed, a confirmation dialog lists:

  • The Stage the instance will return to.
  • Which downstream tasks and actions will be re-opened as a result (based on declared dependencies — see below).
  • Which completed work will be preserved (independent tasks with no declared dependency on the target Stage).

The dialog gives the reviewer the information they need to make an informed decision before committing.

Manual send-back is available to workspace admins and process-flow owners. Ordinary task assignees see the card but cannot initiate the send-back themselves.

3. Resubmitting a rejected gate directly

If the fix for a rejected gate doesn't require revisiting an earlier Stage — for example, re-attaching a missing file that belongs to the gate's own Stage — the approver's task-detail panel offers a Resubmit for decision button directly on the rejected verdict, in every view (Kanban, List, and Wizard). This resets the gate — and, for a sequential flow, every later Stage — back to workable, exactly like a manual send-back to that same Stage, without requiring the reviewer to navigate to a different Stage first.

The approver is prompted for a reason (pre-filled from the original rejection comment, editable), and the platform re-notifies whoever needs to make the next decision. The prior verdict is never silently discarded — it stays visible as a "Previously rejected on…" note once the gate is back to awaiting a decision.


What happens on the round trip

Dependency-driven re-open

When a Stage is re-entered — whether by an approval reject arm or a manual send-back — the platform does not blindly re-open all downstream work. Instead it re-opens only the tasks and actions that declared a dependency on the affected Stage or its tasks.

Independent completed work is preserved. If a parallel track finished and has no declared dependency on the reopened Stage, it stays completed with its original history intact. This prevents unnecessary rework and keeps the audit trail coherent.

Declared dependencies drive re-open precisely. A workflow action that declared metadata.retrieval.inputs: [{ fromTask: "upload-documents" }] knows it depends on the output of the upload-documents task. When that task's Stage is re-entered and the upload-documents action is re-opened, the downstream workflow action is also re-opened — because its input may have changed.

Sequential stage flows are the one exception. If your template uses the default stageFlow: 'sequential', every Stage after the reopen target is always included in the re-open, whether or not you declared a dependency — sequential locking is itself a cross-stage relationship, so a Stage that was only reachable because an earlier one completed is treated as dependent on it. Declared dependencies (fromTask, dependsOn) still matter for parallel / flexible flows, and for reaching further than the immediately-reopened stage's siblings in a sequential flow that also has explicit intra-stage edges.

Authoring guidance: declare your cross-stage dependencies

The re-open logic follows the dependency graph you author, plus — for sequential flows — the stage sequence itself. Undeclared dependencies inside a parallel or flexible flow are invisible to the platform — if a downstream task would logically need to re-run after an upstream Stage is reworked but has no declared dependency on it, the platform will leave it completed.

Declare cross-stage data dependencies explicitly. Use metadata.retrieval.inputs[].fromTask on workflow actions and dependsOn edges on tasks to wire the dependency graph. The more complete the graph, the more precise the re-open blast radius — exactly the right work is redone, no more and no less.

A well-declared graph means:

  • Correct re-open: tasks that depend on reworked data are re-activated automatically.
  • Preserved work: tasks that are independent survive the loop intact.
  • A clear story for reviewers: the confirmation dialog before send-back shows exactly what will change.

Restricting which stages a stage can be sent back to

By default, any Stage can be manually sent back to any earlier Stage in the sequence. If your template has governance constraints — for example, a legal-review Stage should only ever return to the prior Stage, never to Stage 1 — declare allowedReturnStages on the Stage:

import { defineProcessFlow, defineStage } from '@scrydon/sdk-authoring/process-flows'

defineProcessFlow({
  // ...
  template: {
    // ...
    stages: [
      defineStage({
        slug: 'drafting',
        name: 'Drafting',
        transitionMode: 'automatic',
      }),
      defineStage({
        slug: 'legal-review',
        name: 'Legal Review',
        transitionMode: 'manual',
        allowedReturnStages: ['drafting'], // ← only this stage is a valid send-back target
      }),
      defineStage({
        slug: 'sign-off',
        name: 'Sign-off',
        transitionMode: 'manual',
        allowedReturnStages: ['legal-review', 'drafting'], // ← both are valid targets
      }),
    ],
    // ...
  },
})

When allowedReturnStages is set:

  • The Send back to… menu on the Kanban board only offers the listed Stages.
  • The platform enforces this list server-side on the manual send-back route. If a caller attempts to send back to a Stage not in allowedReturnStages, the request is rejected with 403 target_not_allowed. The UI menu is a convenience — the enforcement is authoritative at the API level.
  • The approval reject arm (onReject.returnToStage) is author-declared and is exempt from the allowedReturnStages gate — its target is validated at pack import time instead.
  • Keep each approval's onReject.returnToStage within the Stage's allowedReturnStages so the declared rework loop stays consistent with the governance list.
  • An empty array ([]) means the board offers no manual send-back targets from that Stage, and any manual send-back request will be rejected by the API.
  • Omitting allowedReturnStages entirely means send-back is free-form to any earlier Stage (the default).

All slugs listed in allowedReturnStages must be Stages that appear earlier in the template's stage sequence. The platform validates this at pack import and rejects any forward or self-references.


Authoring a full rework loop

The following example wires both mechanisms — a reject arm on the approval gate and a governance restriction on the sign-off Stage — into a short three-stage contract-review flow.

import {
  defineProcessFlow,
  defineStage,
  defineTask,
  defineAction,
} from '@scrydon/sdk-authoring/process-flows'

export default defineProcessFlow({
  manifestVersion: 1,
  package: { id: 'acme.contract-review', name: 'Acme Contract Review', version: '1.0.0' },
  template: {
    slug: 'contract-review',
    name: 'Contract Review',
    version: '1.0.0',

    stages: [
      defineStage({
        slug: 'drafting',
        name: 'Drafting',
        transitionMode: 'automatic',
      }),
      defineStage({
        slug: 'legal-review',
        name: 'Legal Review',
        transitionMode: 'manual',
        // Legal review can only be sent back to Drafting, not skipped further back.
        allowedReturnStages: ['drafting'],
      }),
      defineStage({
        slug: 'sign-off',
        name: 'Sign-off',
        transitionMode: 'manual',
        // Sign-off can return to either earlier stage.
        allowedReturnStages: ['legal-review', 'drafting'],
      }),
    ],

    taskTemplates: [
      defineTask({
        slug: 'draft-contract',
        stageSlug: 'drafting',
        name: 'Draft contract',
        actions: [
          defineAction({
            name: 'Upload contract draft',
            actionType: 'file_upload',
            isRequired: true,
            metadata: { persona: 'author', acceptedExtensions: ['.pdf', '.docx'] },
          }),
        ],
      }),

      defineTask({
        slug: 'legal-check',
        stageSlug: 'legal-review',
        name: 'Legal check',
        actions: [
          defineAction({
            name: 'Review contract draft',
            actionType: 'approval',
            isRequired: true,
            metadata: {
              persona: 'legal',
              onReject: {
                // Rejection routes back to Drafting — the author must revise.
                returnToStage: 'drafting',
              },
            },
          }),
        ],
      }),

      defineTask({
        slug: 'executive-sign-off',
        stageSlug: 'sign-off',
        name: 'Executive sign-off',
        actions: [
          defineAction({
            name: 'Approve contract',
            actionType: 'approval',
            isRequired: true,
            metadata: {
              persona: 'executive',
              onReject: {
                // Rejection routes back to Legal Review for another pass.
                returnToStage: 'legal-review',
              },
            },
          }),
        ],
      }),
    ],

    personas: [
      { slug: 'author', name: 'Author' },
      { slug: 'legal', name: 'Legal Counsel' },
      { slug: 'executive', name: 'Executive' },
    ],
    voiceTriggers: [],
  },
})

In this flow:

  • The Legal Review approval rejects → back to Drafting (the onReject arm).
  • The Sign-off approval rejects → back to Legal Review for a second pass.
  • A workspace admin can manually send the Kanban card back from Sign-off to either Legal Review or Drafting (per allowedReturnStages), but Legal Review can only go back to Drafting — not bypass it to Sign-off.

Validation checklist

pack validate enforces these rules before a bundle is built:

RuleError you will see
onReject.returnToStage must name a Stage that appears before the current StagereturnToStage "sign-off" is not an earlier stage
onReject.returnToStage must be in allowedReturnStages when the Stage declares itreturnToStage "drafting" is not in allowedReturnStages for stage "legal-review"
Slugs in allowedReturnStages must be earlier StagesallowedReturnStages contains forward reference "sign-off"
onReject is only valid on actionType: "approval"onReject is not valid on actionType "checklist"

On this page

On this page