Data Directives are one of the most powerful tools in the Epicor Kinetic toolbox — and one of the most frustrating when they silently fail. Whether you’re a seasoned Epicor developer or an admin writing your first custom code block, you’ve almost certainly hit a wall where your BPM doesn’t behave the way you expect. Records save when they shouldn’t, exceptions crash the client, or the directive simply never fires.
This guide collects the most common Data Directive errors we encounter in production environments and across the epiusers.help community forums, along with the proven fixes that resolve them.
Understanding the Data Directive Execution Context
Before diving into individual errors, it’s essential to understand when a Data Directive runs. Unlike Method Directives — which wrap around a specific business object method call — Data Directives fire in response to changes on database tables. They execute at three possible points:
- Standard (Pre-Processing): Fires before the transaction is committed. You can validate and reject the save.
- In-Transaction: Fires inside the database transaction. You can read old and new values, but exceptions here will roll back the entire transaction.
- Post-Processing: Fires after the commit. Useful for notifications, logging, or triggering downstream processes — but you cannot prevent the save here.
Many of the errors below stem from choosing the wrong execution context or misunderstanding how the tt (temp table) dataset behaves in each phase.
Error #1: InfoMessage vs. BLException — Why Your Validation Still Saves
This is the single most common “why isn’t my BPM working” question on the Epicor forums. You write a custom code block to validate a field, you see a message pop up on the client — but the record saves anyway.
“I’m using
— Discussion on epiusers.help, BPM & Customization subforumInfoMessage.Publish()to warn users when a required field is blank, but the record still gets saved. What am I missing?”
The answer is straightforward: InfoMessage.Publish() is informational only. It displays a message to the user but does not halt execution. If you need to prevent the save, you must throw an Ice.BLException.
Incorrect (message only, record still saves):
Correct (throws exception, blocks the save):
Key distinction: Ice.BLException is the standard way to raise a user-facing error that prevents the business object from completing its update. Use it in Standard (Pre-Processing) Data Directives or Pre-Processing Method Directives. Throwing it inside an In-Transaction directive will roll back the entire database transaction, which is usually not what you want for a simple validation.
Error #2: “Specified Cast is Invalid” in Custom Code
This runtime error appears when you try to cast a field value to the wrong .NET type. It’s especially common with UD (user-defined) fields, where the underlying SQL type doesn’t match your C# expectation.
“Getting ‘Specified cast is not valid’ on my custom code BPM. The field is a decimal in the database but I’m casting it to int. Took me two hours to figure that out.”
— Discussion on epiusers.help
Common causes:
- Casting a
decimalcolumn tointdirectly (useConvert.ToInt32()instead). - Accessing a UD field via the indexer (
row["MyField_c"]) and casting to the wrong type. - Assuming a
bitcolumn returnsboolwhen it actually comes through asintin some contexts. - Using
(string)cast on a field that could beDBNullornull.
Problematic code:
Safe alternatives:
Pro tip: When in doubt, check the actual column type in Epicor’s Data Dictionary or run a quick SQL query: SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'OrderDtl' AND COLUMN_NAME = 'Quantity_c'. This eliminates the guesswork.
Error #3: NullReferenceException When Accessing tt Rows
The infamous NullReferenceException: “Object reference not set to an instance of an object.” In BPM custom code, this almost always means your LINQ query returned null because no rows matched your filter.
“My data directive works perfectly on new records but crashes with a NullReferenceException on updates. Turns out the RowMod filter was only matching Added rows.”
— Discussion on epiusers.help
The classic mistake:
The fix — always null-check:
Common scenarios where the tt table can be empty or missing your expected row:
- You filtered for
ROWSTATE_ADDEDbut the operation is an update (ROWSTATE_UPDATED). - The Data Directive is on a parent table (e.g.,
OrderHed) but you’re looking for rows inOrderDtl, which may not be in the dataset at that moment. - A Post-Processing directive — by this point the
RowModmay have been cleared. - The directive fires on a delete operation, and you’re only checking for Added/Updated.
A robust pattern that handles all modification states:
Error #4: Data Directive Not Firing After Changes
You’ve built the directive, saved it, tested it — and nothing happens. No error, no message, no evidence it ever ran. This is one of the most disorienting BPM issues because there’s no error to Google.
“I added a new condition to my data directive and now it just doesn’t fire at all. Recycling the app pool fixed it. Wish I’d tried that first instead of spending two hours debugging.”
— Discussion on epiusers.help
Troubleshooting checklist when a Data Directive won’t fire:
- Recycle the Epicor App Server / IIS App Pool. This is the number one fix. Epicor caches BPM definitions aggressively. After saving a new or modified directive, recycle the application pool to force a reload.
- Verify the directive is enabled. Open BPM Maintenance, locate your directive, and confirm the “Enabled” checkbox is checked.
- Check your condition logic. If you have a condition widget before your action, confirm the condition actually evaluates to
truefor your test case. Use the BPM trace or add a temporaryInfoMessage.Publish()at the very start (before any conditions) to confirm the directive fires at all. - Confirm the correct table. Data Directives are tied to a specific database table. If you’re modifying
OrderDtlbut your directive is onOrderHed, it won’t trigger. - Check the execution phase. A Post-Processing directive won’t fire if an error occurred during the save. An In-Transaction directive won’t fire if a Pre-Processing directive blocked the operation.
- Review the Server Event Log. Compilation errors in custom code blocks will silently prevent the directive from loading. Check the Epicor server’s Windows Event Log or the Epicor Admin Console for BPM compilation errors.
Accessing “Old” Values in In-Transaction Directives
A frequent requirement is comparing the previous value of a field to its new value — for example, detecting when an order status changes from “Open” to “Closed.” In-Transaction directives give you access to both the current and previous values through the ttTable and column-changed tracking.
“How do I get the old value of a field in a Data Directive? I need to detect when OrderHed.OpenOrder changes from true to false.”
— Discussion on epiusers.help, recurring question
In an In-Transaction Data Directive, you can use the column-changed pattern to access prior values. The ttTable rows carry the new values, while you can query the database directly (since the transaction hasn’t committed yet, the database still holds the old values):
Important caveat: Accessing Db inside an In-Transaction directive reads the uncommitted state within the same transaction. In most cases this still returns the “old” value because the update hasn’t been applied to the Db context row yet, but behavior can vary depending on the Epicor version and the specific table. Always test thoroughly.
An alternative approach is to use a BPM Data Form (condition widget) with the “Column Changed” option, which lets you detect field changes without custom code. This is more reliable and upgrade-safe.
Best Practice: Prefer Standard Widgets Over Custom C# Code
The Epicor BPM designer includes a rich set of standard action and condition widgets: Set Field, Raise Exception, Send Email, Execute Query, Column Changed, and many more. Before reaching for a Custom Code block, ask yourself whether a standard widget can accomplish the task.
“I had 200 lines of custom C# in a data directive that could have been replaced by three widgets. When we upgraded from 10.2.500 to Kinetic 2022.2, the custom code broke because the Erp assemblies changed. The widget-based BPMs all upgraded cleanly.”
— Discussion on epiusers.help
Why standard widgets are preferred:
- Upgrade-safe: Epicor maintains backward compatibility for standard widgets. Custom code that references internal assemblies or private APIs can break during version upgrades.
- Easier to maintain: Any Epicor admin can read and modify a widget-based flow. Custom C# requires developer expertise.
- Built-in error handling: Widgets handle null checks, type conversions, and transaction management internally. Custom code pushes all of that responsibility onto you.
- Better performance: Widgets are optimized by Epicor. Poorly written custom code — especially code that runs database queries in loops — can introduce serious performance bottlenecks.
When custom code IS appropriate:
- Complex string parsing or mathematical calculations that widgets can’t express.
- Calling external APIs (though Epicor Functions are now the preferred approach for this).
- Advanced LINQ queries across multiple related tables that a single Execute Query widget can’t handle.
- Logic that requires local variables, loops, or branching more complex than the condition widget supports.
Critical: Avoid Calling Business Objects Directly in Data Directives
One of the most dangerous anti-patterns in Epicor BPM development is calling another Business Object (BO) from inside a Data Directive using ServiceRenderer or Ice.Assemblies.ServiceRenderer.GetService.
“We had a data directive on Part that called the PartPlant BO to auto-create plant records. It worked in testing, but in production it caused deadlocks and occasional data corruption when multiple users saved parts simultaneously.”
— Discussion on epiusers.help
Why this is dangerous:
- Deadlocks: Data Directives run inside a database transaction. Calling another BO opens a second transaction. If both transactions need locks on the same tables, you get a deadlock.
- Circular triggers: If you call a BO that modifies a table that has its own Data Directive, you can create infinite loops or deeply nested transaction chains.
- Transaction isolation issues: The called BO may not see uncommitted changes from the current transaction, leading to stale reads and inconsistent data.
- Performance degradation: Each BO call carries overhead — authentication, validation, triggering its own BPMs. Inside a Data Directive that fires on every save, this overhead multiplies quickly.
Safer alternatives:
- Use a Method Directive instead. Method Directives on the parent BO’s
Updatemethod give you more control over timing and don’t carry the same deadlock risks. - Use Epicor Functions. Functions run in their own execution context and can be called asynchronously, avoiding transaction entanglement.
- Use the
Dbcontext directly. For simple inserts or updates to related tables, theDbcontext within an In-Transaction directive participates in the same transaction safely:
When to Use Method Directives Instead
Not every piece of business logic belongs in a Data Directive. Here’s a quick decision framework:
| Scenario | Recommended Directive |
|---|---|
| Validate a field value before save | Data Directive (Standard/Pre-Processing) |
| Auto-populate a field on record creation | Data Directive (Standard/Pre-Processing) |
| Send email notification after save | Data Directive (Post-Processing) |
| Call another BO method as part of the workflow | Method Directive (Post-Processing) |
| Modify the method’s input parameters | Method Directive (Pre-Processing) |
| Complex multi-step integration with external systems | Epicor Function (called from Method Directive) |
The Complete BPM Troubleshooting Checklist
When a BPM Data Directive isn’t working as expected, walk through this checklist before diving into code-level debugging:
- Is the directive enabled? Open BPM Maintenance and verify the checkbox.
- Have you recycled the app pool? Changes aren’t always picked up until the server cache clears.
- Is it on the correct table? Verify you’re targeting the right table (e.g.,
OrderDtlvs.OrderHed). - Is the execution phase correct? Pre-Processing for validation, In-Transaction for old/new comparison, Post-Processing for notifications.
- Are your conditions met? Temporarily remove all conditions to confirm the directive fires at all.
- Are you checking the right RowMod? Include both
ROWSTATE_ADDEDandROWSTATE_UPDATEDunless you intentionally want only one. - Are you null-checking your LINQ results?
FirstOrDefault()returnsnullif no match is found. - Are your casts correct? Use
Convert.ToXxx()instead of direct casts for safety. - Check the server event log. BPM compilation errors appear here, not in the client UI.
- Use BPM tracing. Enable BPM tracing in the Epicor Admin Console to see exactly which directives fire and in what order.
- Test with a fresh record. Cached data or partially saved records can produce misleading behavior.
- Using
BLExceptionfor validation? If you’re usingInfoMessage, the save will still proceed.
Community Solution Spotlight
The epiusers.help community has developed several patterns that are worth adopting as standard practice:
Pattern: Safe Row Access Helper
Wrap your row access in a reusable null-safe pattern to avoid scattering null checks throughout your code:
Pattern: Structured Logging in BPMs
Instead of relying solely on InfoMessage for debugging, write to the Epicor ICE log for persistent, server-side diagnostics:
This approach is invaluable for tracking down issues in production environments where you can’t attach a debugger or reproduce the problem interactively.
Final Thoughts
Epicor BPM Data Directives are extraordinarily powerful, but they demand respect for the execution context they run in. The majority of issues we see in client environments and on the epiusers.help forums come down to a handful of root causes: using InfoMessage instead of BLException, forgetting null checks, using incorrect type casts, and not recycling the app pool after changes.
By following the patterns in this guide — validating with BLException, null-checking every LINQ result, using Convert methods for type safety, preferring standard widgets over custom code, and avoiding direct BO calls in Data Directives — you’ll eliminate the vast majority of BPM headaches before they reach production.
Related Resources & Services
- Epicor BPM Workflow Examples — Real-world BPM automation patterns and templates.
- Data Directive vs Method Directive — When to use each type of business logic.
- Epicor Functions Explained — Move complex logic into reusable server-side functions.
- BPM & Workflow Automation Services — Professional BPM development and optimization.
Need Help with Complex BPM Logic?
Our team has deep experience with Epicor BPM & Workflow Automation. We can debug failing directives, refactor legacy BPMs, and implement robust business process automation.
Request Free BPM Review