Skip to content

How to: Build workflows with ProcessBuilder

The workflow service exposes a Groovy builder DSL for creating OTCS workflow process maps from Content Script. Instead of drawing a map in the Workflow Map Editor, you describe the map as nested builder calls and then validate, preview, or import it into the repository.

Entry point: workflow.getNewProcessBuilder() (since 2.6.0)
Builder type: ProcessBuilder (@ContentScriptAPIObject)
Output: OTCS .map serialization (imported as a workflow definition node)

This page is the practical how-to. For Content Script steps that run inside an existing workflow, see Working with workflows.

What you get

Method Returns Since Purpose
validate() List<ValidationError> 4.0.0 Structured validation (empty list = valid)
toSvg() String 4.0.0 Best-effort SVG diagram of the map
getDefinition() CSResource 2.6.0 Temporary .map resource
createDefinition(parent, title, description, addAttachments?, resetRoles?) CSNode 2.6.0 Import the map into a folder

Typical flow:

def pb = workflow.getNewProcessBuilder()

pb.workflow(title: "My Workflow", description: "Created from Content Script") {
    // packages, start, swimlanes, steps, links…
}

def errors = pb.validate()
errors.each { log.error(it.toString()) }

def parentFolder = docman.getEnterpriseWS()
def wfDef = pb.createDefinition(parentFolder, "My Workflow", "Created from Content Script")

Idempotent recreate

Before calling createDefinition, delete any existing map with the same title if you want re-runs to replace the previous definition:

def existing = docman.getNodeByName(parentFolder, "My Workflow")
if (existing) {
    docman.deleteNode(existing)
}

Mental model

ProcessBuilder extends Groovy FactoryBuilderSupport. Each keyword (workflow, swimlane, form, gateway, …) is a registered factory that builds a DTO and wires it into the parent node.

workflow
├── packages
│   ├── attributes  { attribute* }
│   ├── webforms    { webform* }
│   └── cscripts    { cscript* }
├── start           { go | back | otherwise }*
└── swimlane*
    ├── form / step { go | back | otherwise }*
    ├── gateway     { route*, otherwise?, back? }
    │   └── route   { condition | and | or | group }*
    └── milestone / end

Structural rules worth memorizing:

  • start is a direct child of workflow (not of a swimlane).
  • Human tasks (form / step) and gateways live inside a swimlane.
  • Only start and milestone/end may sit directly under workflow.
  • Inside a gateway, use route { condition … } — never bare go. Use otherwise for the default branch.

Attribute names accept both camelCase and snake_case (onComplete / on_complete, connectionPoints / connection_points).

Minimal example

def pb = workflow.getNewProcessBuilder()
def reviewer = users.getMemberByLoginName("approver1")
def formTemplate = docman.getNodeByName(docman.getEnterpriseWS(), "Approval Form Template")

pb.workflow(title: "Simple Approval", onComplete: "None") {
    packages {
        webforms {
            webform(node: formTemplate, name: "Form")
        }
    }

    start(title: "Start", displayAtInit: true, forms: [[name: "Form", view: "Default"]]) {
        go(to: "Review")
    }

    swimlane(name: "Approvers", assignees: reviewer, form: "Form") {
        form(title: "Review", duration: 2.days, priority: "high") {
            go(to: "Done")
        }
        milestone(title: "Done")
    }
}

def folder = docman.getEnterpriseWS()
pb.createDefinition(folder, "Simple Approval", "")

Workflow attributes

Top-level workflow(...) options:

Attribute Type Default Notes
title String Required for a valid map
description String ""
instructions String "" Shown to participants
custom_message String "" Message at initiation
onComplete String "Delete" Delete, None, Move, Copy
emailAttachmentsAs String "Links" Links, Attachments, None
emailActionConfirmation Boolean false
roleBased Boolean false
skipWeekendFlag Boolean false Skip weekends in due-date math
promptTitle Boolean false Prompt for title at initiation
onInitiateScripts / onCompleteScripts / … List [] Lifecycle Content Scripts

createDefinition extras:

Parameter Default Notes
addAttachments true Attachments work package
resetRoles false Reset role assignments on import

Packages

Declare reusable work packages once, then reference them by name from steps and swimlanes.

Attributes

packages {
    attributes {
        attribute(name: "Priority Level", type: "textpopup",
                  validValues: ["Low", "Medium", "High"], required: true)
        attribute(name: "Due Date", type: "date")
        attribute(name: "Amount", type: "real")
        attribute(name: "Owner", type: "user")
    }
}

Common type values: text, textpopup, textmultiline, date, datepopup, checkbox, integer, integerpopup, real, realpopup, itemreference, tkl, user, userpopup.

WebForms

webform needs a live CSNode (Form Template or Form). The name is the identifier steps use later.

packages {
    webforms {
        webform(
            node: formTemplate,
            name: "ApprovalForm",
            isRequiredForm: true,
            displayAttachments: true,
            viewName: "DefaultView",
            storageMechanism: "Workflow",   // or "Form"
            values: [
                "Requester": [users.current.name]
            ]
        )
    }
}

Content Scripts

packages {
    cscripts {
        cscript(node: notifyScript, name: "Notify")
        cscript(node: routeScript, name: "RouteLogic")
    }
}

Pass either node: (preferred) or name: + scriptId:. When a gateway uses script: "RouteLogic", that string must match the package entry name.

Start step

start(
    title: "Submit Request",
    displayAtInit: true,
    initInSmartUI: true,
    forms: [[name: "ApprovalForm", view: "SubmitView"]],
    attributes: [[name: "Priority Level", required: true]],
    instructions: "Fill the form and submit."
) {
    go(to: "Review")
}

Useful flags: displayAtInit, promptForTitle, setDueDate, authenticate, initInSmartUI.

Swimlanes and assignees

Swimlanes group steps and define who performs them. Properties set on the swimlane (form, view, duration, groupOption, startDate) are inherited by child steps unless overridden.

swimlane(
    name: "Approvers",
    assignees: [approverUser, backupUser],
    form: "ApprovalForm",
    view: "ReviewView",
    groupOption: "member-accept",
    duration: 2.days
) {
    // steps…
}
assignees value Meaning
CSMember Single user or group
[member1, member2] Multi-performer
"task:Step Title" Inherit performer from another step
"form:FormName:FieldName" Performer from a form field
omitted / initiator: true Initiator swimlane

groupOption values: member-accept, accept-maintain, expand-one-level, expand-full.

Resolve members and nodes first

The DSL expects real objects: assignees: takes a CSMember, webform(node:) / cscript(node:) take a CSNode. Resolve them before the builder runs:

def member = users.getMemberByLoginName("jsmith")
if (!member) {
    throw new IllegalStateException("User 'jsmith' was not found")
}
def formNode = dependencies["approval-form"]  // e.g. injected by App Builder

Steps

Form / step (human task)

form and step are synonyms (both create a FormStep).

form(
    title: "Review",
    priority: "high",          // high | medium | low
    duration: 2.days,
    enableEmailAction: true,
    enableAgent: true,
    form: "ApprovalForm",      // optional if swimlane already sets it
    view: "ReviewView"
) {
    go(to: "Decision")
}

Gateway (decision)

Conditional routing uses route + condition. Always provide an otherwise default when possible.

gateway(title: "Decision") {
    route(to: "Approved") {
        condition(form: "ApprovalForm", field: "Approved", op: "=", value: "Yes")
    }
    route(to: "Escalate") {
        condition(form: "ApprovalForm", field: "Amount", op: ">=", value: "1000")
        and {
            condition(form: "ApprovalForm", field: "Priority", op: "=", value: "High")
        }
    }
    otherwise(to: "Rejected")
}

Condition sources:

Kind Attributes Example
Form field form, field, op, value condition(form: "F", field: "Status", op: "=", value: "OK")
Workflow attribute attribute, op, value condition(attribute: "Priority Level", op: "=", value: "High")
CS step outcome outcome, op, value only on gateway(script: …)
Status general: "Status", op, value "ok", "step-late", "workflow-late"

Operators: =, !=, >, <, >=, <=, LIKE, NOT LIKE. Combine with and { }, or { }, and group { }.

Content Script step

Add script: on a gateway to create an automated Content Script step (instead of a pure evaluate step):

gateway(title: "Auto Route", script: "RouteLogic", background: true) {
    route(to: "Success Path") {
        condition(outcome: "success", op: "=", value: "ok")
    }
    route(to: "Error Path") {
        condition(outcome: "error", op: "=", value: "failed")
    }
    otherwise(to: "Fallback")
}

Outcome kinds: success, error, number, string. outcome: conditions are invalid on gateways without script:.

Milestone / end

milestone(title: "Approved")
end(title: "Rejected")   // synonym for milestone
Keyword Role
go(to: "…") Forward transition (not inside gateways)
back(to: "…") Loop back
otherwise(to: "…") Default / else branch
route(to: "…") { … } Conditional edge (gateway only)

Step targets resolve by title. If titles collide across swimlanes, qualify them:

go(to: "Approvers:Review")

Optional visual routing:

back(to: "Review", connection_points: "left,bottom")
go(to: "Next", connection_points: "bottom+20,top")

Validation and SVG preview

def errors = pb.validate()
def fatal = errors.findAll { it.severity == ValidationError.Severity.ERROR }
if (fatal) {
    fatal.each { log.error(it.toString()) }
    return
}

String svg = pb.toSvg()   // best-effort; call validate() first

Common codes: EMPTY_WORKFLOW, MISSING_TITLE, DEAD_LINK, MISSING_CONDITION, INVALID_OUTCOME_CONDITION, DUPLICATE_TITLE, ORPHAN_STEP, MISSING_FORM_REF, GATEWAY_MISSING_DEFAULT_PATH, UNREACHABLE_STEP.

Complete example

def pb = workflow.getNewProcessBuilder()

def formTemplate = docman.getNodeByNickname("approval-form-template")
def notifyScript = docman.getNodeByNickname("wf-notify")
def approver = users.getMemberByLoginName("approver1")
def folder = docman.getNodeByNickname("workflows-folder")

pb.workflow(
    title: "Document Approval",
    description: "Two-step approval",
    onComplete: "None",
    skipWeekendFlag: true
) {
    packages {
        attributes {
            attribute(name: "Priority Level", type: "textpopup",
                      validValues: ["Low", "Medium", "High"])
        }
        cscripts {
            cscript(node: notifyScript, name: "Notify")
        }
        webforms {
            webform(node: formTemplate, name: "ApprovalForm", isRequiredForm: true)
        }
    }

    start(
        title: "Submit",
        displayAtInit: true,
        initInSmartUI: true,
        forms: [[name: "ApprovalForm", view: "SubmitView"]],
        attributes: [[name: "Priority Level", required: true]]
    ) {
        go(to: "Review")
    }

    swimlane(name: "Approvers", assignees: approver, form: "ApprovalForm", view: "ReviewView") {
        form(title: "Review", priority: "high", duration: 2.days,
             enableEmailAction: true, onReadyScripts: [notifyScript]) {
            go(to: "Decision")
        }

        gateway(title: "Decision") {
            route(to: "Approved") {
                condition(form: "ApprovalForm", field: "Approved", op: "=", value: "Yes")
            }
            otherwise(to: "Requestor Review")
        }

        milestone(title: "Approved")
    }

    swimlane(name: "Requestors", initiator: true, form: "ApprovalForm") {
        form(title: "Requestor Review", view: "SubmitView", duration: 1.day) {
            back(to: "Review", connection_points: "left,bottom")
        }
    }
}

pb.validate().each { log.error(it.toString()) }

def existing = docman.getNodeByName(folder, "Document Approval")
if (existing) {
    docman.deleteNode(existing)
}

def wfDef = pb.createDefinition(folder, "Document Approval", "Created programmatically")
log.info("Created workflow definition ${wfDef.ID}")

Programmatic generation

The same DSL is the target language of the App Builder / Generative Service workflow pipeline: an intermediate workflow IR is rendered to Groovy that calls workflow.getNewProcessBuilder(), builds the tree, validates, and calls createDefinition.

When generating scripts, follow the same binding rules the DSL itself requires:

  1. Resolve dependency nodes (webforms / cscripts) into local variables before the builder block.
  2. Resolve swimlane member assignees with users.getMemberByLoginName(…).
  3. Quote string refs for script:, form names, and step titles.
  4. Emit route + condition (never go) inside gateways; prefer an otherwise fallback.
  5. Qualify step targets as Swimlane:Title when titles are not unique.

Skeleton of a generated script:

// Dependency nodes (e.g. injected as a Map by the save pipeline)
def dep_approval_form = dependencies['approval-form']
if (!dep_approval_form) {
    throw new IllegalStateException("Dependency 'approval-form' is not available")
}
def member_approver1 = users.getMemberByLoginName('approver1')
if (!member_approver1) {
    throw new IllegalStateException("User or group 'approver1' was not found")
}

def pb = workflow.getNewProcessBuilder()

pb.workflow(title: 'Document Approval', onComplete: 'None') {
    packages {
        webforms {
            webform(node: dep_approval_form, name: 'ApprovalForm')
        }
    }
    start(title: 'Submit', forms: [[name: 'ApprovalForm', view: 'Default']]) {
        go(to: 'Review')
    }
    swimlane(name: 'Approvers', assignees: member_approver1, form: 'ApprovalForm') {
        form(title: 'Review') {
            go(to: 'Done')
        }
        milestone(title: 'Done')
    }
}

def errors = pb.validate()
errors.each { log.error(it.toString()) }

def existingMap = docman.getNodeByName(parentFolder, 'Document Approval')
if (existingMap) { docman.deleteNode(existingMap) }

def wfDef = pb.createDefinition(parentFolder, 'Document Approval', '')