Every so often a client asks us to do something that sounds trivial, we go looking for the built-in button, and… there isn't one. This is one of those stories. The ask: stamp every record routed through an approval workflow with a true random number — not a sequential ID, not a GUID, a real 1-to-9999 random value for a regulator's audit sample. The fix: Laserfiche Workflow, a scripted activity, and one of the oldest modules in the .NET Standard Library.
THE PROBLEM
Why a random number matters
Our client's compliance team pulls monthly samples for review. To keep the sampling statistically defensible, each document needed an attached random value that auditors could sort, filter, and select from without any correlation to the order documents were processed in. Sequential IDs leak information (time of day, batch size, workload); GUIDs are unique but not uniformly random across a small range. They needed the number to be small, bounded, and repeatable nowhere.
THE LIBRARY
One class, two gotchas
If you've written .NET before you know exactly where this is going: System.Random. It's the right tool, but it comes with two classic traps that bite anyone who only uses it occasionally.
The first is the seed. new Random() without an argument seeds from the system clock. Instantiate two Random objects in the same tick — which happens when a workflow fires three activities in rapid succession — and you'll get the same "random" number back from each. The second is scope. Random is not thread-safe; sharing a single instance across parallel branches of a workflow will occasionally return zero or duplicate values.
"The laziest bug in .NET is two Random objects created in the same millisecond. Test your workflow at speed — not by hand, one click at a time."
THE SCRIPT
Dropping it into Laserfiche Workflow
Laserfiche Workflow ships with a Script activity that gives you a full C# (or VB.NET) code editor against the workflow's runtime, including its Entry, Properties, and Tokens APIs. We dropped in a small class that keeps a single seeded Random instance alive for the life of the workflow run, draws one value between 1 and 9,999, and writes it back as a workflow token the rest of the pipeline can consume:
// Script activity — Laserfiche Workflow
private static readonly Random _rng =
new Random(Guid.NewGuid().GetHashCode());
protected override void Execute()
{
int value = _rng.Next(1, 10000); // inclusive 1, exclusive 10000
SetTokenValue("AuditRandom", value);
}
Two things are doing work there. Seeding with Guid.NewGuid().GetHashCode() guarantees a different seed every run, regardless of how many workflows start simultaneously. Declaring the Random as static readonly means the activity reuses the same generator across calls within the same workflow assembly, preventing duplicate seeds when workflows parallelize.
THE WORKFLOW
Wiring it to the document
Downstream of the script activity we used a standard Assign Field Values step to stamp the AuditRandom token onto the document's metadata template. From there the value flows into search, reports, and the compliance team's audit dashboard without any further handling.
For belt and suspenders we also route the value through a quick Conditional Decision — if for any reason the token comes back empty or zero, the workflow sends itself to an error queue instead of writing bad data to the file. In production we've run hundreds of thousands of documents through this path and caught exactly three errors, all caused by a separate script activity timing out, not the RNG itself.
WRAPPING UP
The lesson, generalized
Laserfiche Workflow is impressively complete out of the box, but the escape hatch — a C# script activity with full .NET — is what lets it cover the long tail of weird, specific requests. The whole "generate a real random number" job took about twenty minutes to build and another thirty to pressure-test. The key was knowing why the obvious approach doesn't work, not writing more code.
If you're staring at a workflow that can't quite do the thing your auditor is asking for, the script activity is almost always where the answer lives. And if you're not sure what to write in there, we do this every week — let's talk.