Workflow Anti-Patterns#
The designs on this page save, start, and look right on the canvas, and then do the wrong thing at run time. Neither designer judges what a workflow means, so most of these mistakes show as a wrong result, as an error on every occurrence, or as a workflow that never starts. Each entry describes what the design looks like, why it fails, and what to build instead.
Info
To start from a symptom instead of a design, see Troubleshooting by Symptom.
Choosing the Task#
A File Task Chosen by Its Name#
- What it looks like - a file integration uses the tasks found by their name, such as Move, Read File, or New File Event.
- Why it fails - the file driver tasks are deprecated in 12.0. They duplicate the commands and events the file driver already declares and tie the workflow to one task library. In Data Flow the 12.0.0 migration replaces them, except Read File; Control Flow workflows only get a warning. On Fuji Nexim Request and Send Fuji Nexim Reply are deprecated the same way.
- Build this instead - use Execute Equipment Command on the driver's command, such as
MoveorReadFile, and On Equipment Event onOnNewFile,OnFileChanged, orOnFileDeleted. See the file driver tasks migration guide ↗.
The Equipment Reached Around the Driver Definition#
- What it looks like - the driver definition is left empty, and the workflow subscribes to or publishes the equipment's addresses, such as MQTT topics, with Send Notification to Driver or Subscribe In Driver.
- Why it fails - the driver reaches
Communicatingand subscribes only to what its definition declares, so nothing arrives. The drivers' legacy extension topics are deprecated in 12.0 and log a[DEPRECATION]warning on every message. - Build this instead - declare the equipment's properties, events, and commands in the Automation Driver Definition, and use the equipment tasks. For entries known only at run time, use Custom Driver Definition.
One Task Instance for Every Occurrence#
In Data Flow, each task is one instance for the life of the workflow, and every value that arrives replaces the previous one, whichever occurrence sent it. A Control Flow branch is built for one occurrence and released when it ends.
A Fast Event, or a Value Read After a Wait, in Data Flow#
- What it looks like - an event that can fire in bursts is handled in Data Flow, or a value of the event is linked straight into a task that runs after a Sleep or another slow step.
- Why it fails - the value reaches the far task at once and waits there, while the activation takes the longer path. An occurrence that arrives in between replaces the value, so a run reads the next item's value, or
<null>. The log warnsTask '<page>\<task>' input '<input>' will receive value '<new>' instead of '<old>'. - Build this instead - build the handler in Control Flow, where each occurrence has its own branch and its own values. In Data Flow, take each value from the task just before it, not from the event.
Several Values in One Moment#
- What it looks like - a loop or a Code task emits several values into a Data Flow task, and each value is expected to run the task once.
- Why it fails - values that reach a task in the same moment merge into one change, and the last value of each input wins. The task runs once.
- Build this instead - do the per-item work in a Control Flow Foreach, where each pass is a step of its own.
Values Without an Activation#
- What it looks like - a Data Flow task receives its values on links, and nothing is linked into Activate, such as a Log Message fed only its message.
- Why it fails - a task runs when a value reaches Activate. In 12.0, Log Message, Switch, and XPath Expression Evaluator also run only from Activate. The 12.0.0 migration adds these links in Data Flow for Log Message and Switch, not for XPath Expression Evaluator.
- Build this instead - link the Success of the task that feeds the values into Activate, so values and activation arrive together. See the task normalization migration guide ↗.
A Join Activated From the Trigger#
- What it looks like - a Synchronize waits for two values, and its Activate is linked from the trigger that starts the work.
- Why it fails - the trigger's Success reaches every link at once, so the activation arrives before the values. An activation that finds the set incomplete is not remembered, and the join never emits.
- Build this instead - activate the join from the task that produces the last value, or from every producer with Emit when all defined on.
A Value and Its Activation From Two Triggers#
- What it looks like - a value comes from one trigger, and the moment to use it from another, such as a file name from an event and the call from a timer.
- Why it fails - each occurrence is an execution context of its own. Tasks such as Workflow, Synchronize, and Code match a value and an activation by context, so they never meet, and the log warns
Execution context '…' has expired. Lost inputs: …. - Build this instead - keep one trigger per chain. Store the value with Store Data when it arrives and read it with Retrieve Data when the other trigger fires.
State in the Wrong Place#
A Variable in the Code Used as a Store#
- What it looks like - a Code task keeps a count or the last value in a variable outside its class, or in a member added to
framework. - Why it fails - the variable lives as long as the task: a Data Flow task lives as long as the workflow, and a Control Flow task lives for one branch. The count climbs in one designer and resets in the other, and no other task can read it.
- Build this instead - keep values that outlive a run in the data store, with Store Data and Retrieve Data. Keep a running total in Control Flow when events arrive in bursts.
A Persistent Value Under a Shared Identifier#
- What it looks like - Store Data keeps a
Persistentvalue under a generic identifier, such ascount. - Why it fails - every workflow of the controller shares the store, and a
Persistentvalue survives restarts. A workflow reads a value that another or an earlier workflow left, and is wrong from its first run. - Build this instead - name each identifier after its workflow, and give every Retrieve Data a default that you test.
Settings Read as Something Else#
A Setting Whose Name Starts With "auto"#
- What it looks like - Automatic Setup is read as "connect automatically", or every trigger is assumed to be enabled by default.
- Why it fails - Automatic Setup answers the setup phase, and only on an
OnInitializetrigger. The setting that enables a trigger, and its default, differ from task to task. - Build this instead - read each setting and its default in the task's reference page, and set it explicitly. On Driver Event, a value that converts to
trueon Activate starts the trigger, and any other value stops it.
An Expression Where a Value Is Stored#
- What it looks like - a
{{ }}expression is written in a setting, or in a port's default value. - Why it fails - a setting is applied when the branch is built, before any task runs, and a default value is used as written. The task receives the text of the expression. Only a few settings evaluate their own expression.
- Build this instead - write the expression on the input row of the same name, and in Data Flow link the value. See the settings that accept an expression in Control Flow Expressions.
JavaScript Inside an Expression#
- What it looks like - a condition written with
&&,||, or!, or a call to a JavaScript string method such aslower(). - Why it fails - expressions are MathJS. Every occurrence fails with
Failed to parse expressionorUndefined function. - Build this instead - write
and,or, andnot, compare values explicitly, and move string handling to a converter or a Code task.
Control Flow Structure#
A Trigger Placed Inside a Branch#
- What it looks like - a trigger sits inside another task's branch, for example an equipment event inside a function "because the equipment is already in that state".
- Why it fails - reaching the trigger registers a listener, and its handler waits for the next occurrence. Each pass registers one more listener, and the handler does not run when the branch passes.
- Build this instead - place every trigger at the root, with its handler holding the work. For a state that already holds, also do the work from the connection's own handler.
A Try Without a Catch, or a Catch-All That Alerts for Everything#
- What it looks like - a Try..Catch has no catch branch, or an Unhandled Error task raises an alert for every report it receives.
- Why it fails - a try without a catch logs
No onError/catch workflow is configured to be executed, so continue to next task, and the flow continues as if the work had succeeded. In Control Flow, Unhandled Error receives every error before the catch runs, plusExecution Return invokedandExecution Break invoked, and by default it hears every workflow of the controller. - Build this instead - give every Try..Catch a catch. Filter the return and break messages in the catch-all, and set Handler Mode to
ThisWorkflowwhere it must hear its own workflow only.
Subworkflows#
An End Straight Off the Start#
- What it looks like - the Workflow End of a subworkflow is fired straight from its Workflow Start, beside the tasks that do the work.
- Why it fails - the first end to fire finishes the call and drains the subworkflow. Tasks fed in the same moment have no guarantee of running first.
- Build this instead - fire each end from the task that finishes the work.
A Clock, a Listener, or Long Work Inside a Subworkflow#
- What it looks like - a subworkflow holds a timer, an equipment event, or a long Sleep, or it is expected to run on its own.
- Why it fails - in 12.0 a Data Flow subworkflow does not start with the engine. Each call builds its tasks and destroys them when the first end fires or the execution expires, so a trigger inside lives only for that call.
- Build this instead - keep triggers and clocks in the calling workflow, call the subworkflow once per occurrence, and set Execution Expiration Time to what the work takes. See the subworkflow migration guide ↗.
Reacting to the Equipment#
Equipment Work on Every Connection Change#
- What it looks like - the
OnCommunicationChangehandler reads the equipment or sends it a command, with no test of the new state. - Why it fails - the event fires on every state, such as
Connecting,Setup, andDisconnected, and most of those runs fail because the equipment cannot answer. - Build this instead - test the state for
Communicatingbefore the equipment work. Put work that belongs to the start of a connection after the setup is answered.
A Command That Raises the Event It Answers#
- What it looks like - a handler answers an equipment event with a command, and the command's effect raises that event again.
- Why it fails - the event does not say who caused the change, so the handler answers its own command without end.
- Build this instead - read in the equipment documentation which events each command raises, and guard the command with a test of the state, or with a mark kept in the data store.
SetupSuccess Right After Connect#
- What it looks like - the Setup workflow sends
Connectand thenSetupSuccessin the same chain. - Why it fails -
SetupSuccessarrives before the setup phase, and the driver ignores it withIgnoring the SetupComplete action because the state is not 'Setup'. With Automatic Setup off, nothing answers the setup phase, and the connection fails at the Setup Timeout. - Build this instead - send
SetupSuccessfrom theOnSetuphandler, or keep Automatic Setup on theOnInitializetrigger.
Two Instances Watching One Folder#
- What it looks like - two instances, often of two integrations under one Automation Manager, watch the same folder.
- Why it fails - both drivers report every new file, and whichever moves it first leaves the other failing with
ENOENT. - Build this instead - give each instance its own folder, read from MES per entity in the Setup workflow.
A Readable Canvas#
Nodes That Read Alike#
- What it looks like - two Code tasks, evaluators, or Execute Service Call tasks in one workflow show only their task name, or Control Flow blocks keep a caption that does not say what the step does.
- Why it fails - a reader must open each task's settings to tell the steps apart.
- Build this instead - give each Data Flow task a name or a description of a few words. In Control Flow, write a caption that shows the values the step uses, with
{{input.<name>}}for an input and{{<name>}}for a setting. A caption shows values; it does not evaluate an expression. See Tasks.
Every Driver in One Color#
- What it looks like - a controller with several drivers keeps the default color for each.
- Why it fails - each equipment task takes the color of its driver, so the canvas does not show which equipment each task talks to.
- Build this instead - give each driver its own color when you add its driver definition to the Automation Controller.
Several Use Cases in One Workflow#
- What it looks like - new handling is added to a workflow that already has a job, often the Setup workflow.
- Why it fails - the Setup workflow configures and connects every driver, so a task added there changes the start-up of every use case. A workflow that does several things is hard to read, test, and replace.
- Build this instead - give each use case its own workflow, and keep logic that several workflows share in one function or subworkflow.