dotCMS / dotCMS/core

Workflow $workflow.task is null on first fire of new content for all Velocity-rendering actionlets — engine persists workflow_task after the actionlet loop runs

Open
#35,570 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

OKR : Customer Support Team : Maintenance
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Problem Statement

On the first fire of a workflow action against brand-new content (no pre-existing workflow_task row), processor.getTask() is null inside every sub-action that exposes the WorkflowProcessor to a Velocity context. As a result, custom VTL templates referencing $workflow.task.id (or $workflow.task.*) silently render empty, producing broken output such as workflow links of the form /dotAdmin/#/c/workflow/ (no task id appended).
Because the templates use Velocity's quiet reference syntax $!{...}, the failure is silent — the email/message goes out with a broken link and nobody notices until end users complain.

This violates the documented contract in NotifyAssigneeActionlet.getHowTo():

"Both the subject and message are parsed Velocity, and have access to a $workflow object that gives them $workflow.task, $workflow.nextAssign, $workflow.action, $workflow.step, etc."

$workflow.task is advertised as available but is null on first fire of new content.

Root Cause:

WorkflowProcessor constructor sets the task at construction time only:

// dotCMS/src/main/java/com/dotmarketing/portlets/workflows/model/WorkflowProcessor.java:164
task = getWorkflowAPI().findTaskByContentlet(contentlet);

For new content, findTaskByContentlet returns null.
Inside WorkflowAPIImpl.fireWorkflowPostCheckin (lines ~2465–2500), all sub-actions execute before the task is persisted:

for (WorkflowActionClass actionClass : ...) {
    actionlet.executeAction(processor, params);   // ← ran with task = null
}
this.saveWorkflowTask(processor);                 // ← persists task & calls processor.setTask(task)

saveWorkflowTask (lines ~2543–2575) carries an in-code comment that already acknowledges the timing problem:

if (null == processor.getTask()) {
    processor.setTask(task); // when the content is new there might be the case
                             // than an action is waiting for the task in some commit listener
}
Steps to Reproduce
  • Create a workflow whose default action contains an Async Send an Email (or Send an Email / Send a Message / Notify Users / Notify Assignee with a custom template) sub-action.
  • Use a custom email/message body that references $!{workflow.task.id}, e.g.:

<a href="$!{dotCMSURL}/dotAdmin/#/c/workflow/$!{workflow.task.id}">Go to My Tasks</a>

  • Fire the action against brand-new content via REST:

POST /api/v1/workflow/actions/default/fire/NEW

  • Inspect the email/message: the link is rendered as /dotAdmin/#/c/workflow/ (empty task id).
  • Fire any subsequent action on the same content (UI or REST). The link now renders correctly.

The deciding factor is whether a workflow_task row exists at fire time, not REST-vs-UI. Two REST fires in sequence will reproduce: first one broken, second one correct.

Existing Patterns in Codebase That Already Handle This
One places in the same codebase get this right — they should be the templates for the fix:

  1. CommentOnWorkflowActionlet.java:78–95 — commit-listener pattern (recommended)
if (null == processor.getTask() || processor.getTask().isNew()) {
    HibernateUtil.addCommitListener(() -> {
        if (null != processor.getTask()) {
            if (!UtilMethods.isSet(comment.getWorkflowtaskId())) {
                comment.setWorkflowtaskId(processor.getTask().getId());
            }
            this.saveComment(contentlet, comment);
        }
    });
}

Defers the work until after saveWorkflowTask has run and processor.setTask(task) has populated the field. Works generically for any actionlet whose dispatch can be deferred to post-commit.

Acceptance Criteria
  • Firing a workflow action that includes a Velocity-rendering sub-action against brand-new content (no prior workflow_task row) via POST /api/v1/workflow/actions/default/fire/NEW produces a rendered template where $workflow.task is non-null and $workflow.task.id resolves to the persisted task id.

  • The above holds for all of the following sub-actions when the rendered template references $workflow.task.id:

  • EmailActionlet ("Send an Email")

  • AsyncEmailActionlet ("Async Send an Email")

  • MessageActionlet ("Send a Message")

  • LargeMessageActionlet ("Send a Large Message")

  • NotifyUsersActionlet ("Notify Users")

  • NotifyAssigneeActionlet ("Notify Assignee") — both default template and custom override

  • Any custom Velocity-script actionlet authored against the documented $workflow contract

  • The same scenario triggered from the backend UI continues to work as before (no regression on currently-working paths).

  • Firing the same action twice on a brand-new contentlet (REST then REST, REST then UI, UI then UI, UI then REST) produces a correct task-id link on both fires — including the first.

  • The persisted task id in the rendered template matches the row eventually written to workflow_task (i.e., not a transient/throwaway id).

dotCMS Version

dotCMS 24.04.24_lts_v23 — reproduces
dotCMS 24.12.27_lts_v4 — reproduces
latest

Severity

High - Major functionality broken

Links

https://helpdesk.dotcms.com/a/tickets/35722

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with WorkflowProcessor.java around its constructor and WorkflowAPIImpl.fireWorkflowPostCheckin/saveWorkflowTask to trace when the task is assigned relative to actionlet execution. Compare the commit-listener approach in CommentOnWorkflowActionlet.java. Done when the listed Velocity-rendering actionlets expose the persisted task id on the first fire for new content, without regressing UI-triggered workflows.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.