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
Nobody has claimed this yet.
- 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:
- 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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