Some PDIs are currently unavailable, and PDI actions are paused. View the latest updates here. Read More

PlanRecursionException in Bidirectional Stage-Based Flows: Least Invasive Solution?

ClaudioG4376288
Tera Contributor

Hi ServiceNow Community,

I am looking for advice on the least invasive and most upgrade-safe solution to address a PlanRecursionException, without increasing the platform recursion limit.

We have implemented a stage-based process on a custom Case table.

When a Case is created, the process is managed through multiple record-triggered flows operating on the same Case record.

The design works as follows:

  1. Each flow is associated with a specific stage.
  2. A flow is triggered when the stage field changes to the corresponding value.
  3. The flow performs the activities required for that stage.
  4. At the end, the flow updates the same Case record by setting the stage field to the value expected by the trigger of the next flow.

In a simplified forward path:

 

Case created
→ Stage changes to A
→ Flow A starts
→ Flow A performs its actions
→ Flow A sets Stage to B
→ Flow B starts
→ Flow B performs its actions
→ Flow B sets Stage to C

 

However, the process is bidirectional. A Case can both advance and move back to a previous stage.

For example, when an approval is rejected or not granted, the current flow may set the stage field back to a previous value:

 

Flow A → Stage B → Flow B → approval rejected
→ Stage is set back to A
→ Flow A is triggered again

 

Therefore, returning to an earlier stage is an expected business scenario, not necessarily an unintended technical infinite loop. Nevertheless, this backward transition re-enters a flow that has already been part of the current execution chain, which appears to be the reason why ServiceNow detects plan recursion.

The preceding flow is shown as Completed, and the Case record is updated correctly. However, the flow associated with the new stage is not created in the Flow Executions list.

The system log contains the following error:

 

Failed to run flow trigger for trigger runner with identifier:
1837797e3b10c310c866b31864e45ad4
of class:
com.glide.flow_trigger.engine.RecordTriggerRunner
 
com.snc.process_flow.exception.PlanRecursionException:
Plan recursion has been encountered

 

The stack trace shows that the error occurs after a RecordUpdateOperation.

The apparent sequence is:

 

The current flow updates the Stage field
→ the Case record is updated
→ the record trigger associated with the new stage is evaluated
→ ServiceNow attempts to start the corresponding flow
→ the Flow Designer recursion check blocks the execution
→ no new flow context is created
`

 

This is particularly relevant in the rejection path:

 

Stage A → Stage B → approval not granted → Stage A

 

From a business perspective, the Case must legitimately return to Stage A and repeat some or all of the processing associated with that stage. From the Flow Designer engine perspective, however, this seems to be considered recursion because the same flow plan is started again within the existing execution chain.

We would prefer not to:

  • increase the recursion limit;
  • redesign the entire process;
  • remove the ability to return to a previous stage;
  • lose the modularity provided by having one flow for each stage.

What would be the recommended and least invasive approach for this scenario?

 

Thank you.

3 REPLIES 3

Vikram Reddy
Tera Guru

Hey @ClaudioG4376288,

 

// End of Flow A / Flow B, replace the direct
// "Update Record" step on Case with:

gs.eventQueueScheduled('case.stage.transition', current, current.stage.toString(), '', gs.now());

Point the downstream flow's trigger at the sysevent table instead of Case (add sysevent to sn_flow_designer.allowed_system_tables if it isn't selectable yet), filtered on that event name. The stack trace names com.glide.flow_trigger.engine.RecordTriggerRunner specifically, that's the piece walking the current plan's execution chain and blocking re-entry into a flow already in it. An event fired this way runs on its own transaction, so the next stage's flow starts a fresh execution chain instead of nesting inside the one Flow A is still part of. No recursion limit change, no redesign, each stage keeps its own flow.

 

Thank you,
Vikram Karety
Octigo Solutions INC

@Vikram Reddy thank you for the answer, I was trying your solution, the fact is that my flows are in a scoped app, at the moment adding the sys_Event_table in the allowed_system_tables works only if the flows are in global. So thinking to copy them in global for test purposes. What do you think?

Hi @ClaudioG4376288,

 

I'd hold off on that. What you're describing (copying the flows into global "for test purposes") has a way of quietly becoming permanent, and moving an app between global and scoped isn't really reversible on this platform, there's no supported migration tool for it, so if it sticks you're looking at rebuilding rather than moving back. You'd also end up maintaining two live copies of the same stage logic, which is the opposite of the least invasive, upgrade safe goal you started with.

What you hit isn't a config gap on your side. Sysevent is a core system table, and scoped flows are blocked from using core tables as a trigger source regardless of allowed_system_tables, that property lives in global scope and doesn't actually grant your app cross scope access to the table, it just controls whether the table shows up as selectable when the flow itself is global. So the trigger picker never being populated for a scoped flow is expected platform behavior, not something you're missing a step on.

The part that already works for you, the gs.eventQueueScheduled call from inside Flow A / Flow B, is fine to keep, scoped apps are allowed to write to sysevent. What you don't want is a scoped flow trying to trigger off it directly. Instead of moving the stage flows, keep them exactly where they are and add one small bridge in global scope that reacts to the event and hands off to the scoped flow by script rather than by native trigger:

  1. Leave the gs.eventQueueScheduled call in Flow A / Flow B as is.
  2. In global scope, create a Business Rule on sysevent, after insert, filtered on name = case.stage.transition.
  3. From that Business Rule, invoke your scoped stage flow by name through the Flow API, running it in the background so it still gets its own execution context:
// Global scope Business Rule on sysevent, after insert
var inputs = {};
inputs['case_id'] = current.parm1; // sys_id you passed when queuing the event
sn_fd.FlowAPI.getRunner()
    .flow('x_yourscope.flow_stage_a')
    .inBackground()
    .run(inputs);

This keeps the one flow per stage design and your app fully scoped, the only thing living in global is a tiny dispatcher Business Rule, not your business logic. Because the event queue processor already runs the trigger in its own scheduled job, calling the flow inBackground from there should preserve the fresh execution chain that fixed the recursion in the first place, worth confirming in your sub-prod instance by checking sys_flow_context after a rejection loop to make sure Flow A's second run shows a new context id rather than nesting into the old one.

One more thing worth checking before you build the bridge: on the sysevent table's Application Access record, "Accessible from" needs to be set to "All application scopes" if it isn't already, otherwise even this global Business Rule read on the event parm data could get blocked depending on how strict your instance's cross scope policy is set.

 

Thank you,
Vikram Karety
Octigo Solutions INC