01 What WorkPath Is
WorkPath (branded in products as the WrkEngine) is a low-code application platform I built for standing up data-heavy, workflow-driven line-of-business systems without writing a bespoke app each time. Instead of compiling a fixed domain model, WorkPath stores its schema as data: a customer's objects, properties, validation rules, states, roles, forms, menus, and workflows are all runtime definitions held in MongoDB and interpreted by a generic engine. You build a new application by declaring it, not by coding it.
Atleo โ the PLM/compliance product โ is one such declaration: a client definition (DemoPLM/ProductLifeMgmt) installed onto WorkPath. The engine underneath is industry-agnostic; the same platform could back a different vertical by swapping the object and workflow definitions. This page is about the platform itself, not any one client built on it.
02 Layered Architecture
WorkPath is strictly layered: abstractions and models at the bottom, storage and the dynamic-object engine in the middle, and the workflow engine, services, host, and background runner on top. Everything that varies per customer lives in data, not in these assemblies.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HOSTS โ
โ WorkPath.Www (ASP.NET Core) ยท Blazor UI ยท Services.API (REST) โ
โ WorkPath.Runner (background task host) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ENGINES & SERVICES โ
โ Workflow (phase/step) ยท Services ยท Permissions ยท Searching โ
โ Reporting ยท Forms ยท Tasks ยท Messaging (OpenCqrs/ServiceBus) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DYNAMIC OBJECTS (the heart of the platform) โ
โ Template ยท Property (ร27 types) ยท Query ยท Permissions ยท Script โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FOUNDATIONS โ
โ Abstraction ยท Models ยท Specifications ยท Utilities โ
โ DataAccess (MongoDb / InMemory) ยท DataAccess.MongoDb.Mapping โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
03 The Dynamic Object Model
The core abstraction is the DynamicObjectTemplate โ a runtime-defined entity type โ and the DynamicObject, an instance of it. A record is backed by a raw ExpandoObject persisted to a MongoDB collection; the template supplies meaning: which properties exist, their types, validation, states, and permissions. Every record also carries a set of system fields the engine manages itself.
__object_name โ which template this record belongs to __object_state โ current state-machine position __display_name โ computed from the template's name formula __version โ optimistic version, bumped every save __created_by / __created_date / __last_updated_by / __last_update_date __parent โ set when a record is cloned
A property is far richer than a column โ the engine ships ~27 property types:
- Scalars & lists โ String, Text, Int, Double, Currency, Boolean, Date, ChoiceList / MultiChoiceList, Tag, Regex.
- Formula โ a Jint script evaluated against the record (record.*), producing a typed value for roll-ups and derived fields.
- AutoIncrement โ formatted sequence values (e.g. F00000000.##).
- Reference / ReferenceList / ManyToMany โ typed relationships with inline-form vs. modal rendering and searchable sub-properties.
- Query โ a live, bound query: a Jint expression builds a Mongo filter at read time, so a property surfaces related records dynamically.
- Document / File / Image โ GridFS-backed attachments; Contact / ContactList โ references to users/roles.
04 The Save Lifecycle
ProcessAndSave is the spine of a write. Computed properties run through the embedded Jint engine with the record graph bound into scope, so derived values โ cost roll-ups, totals, display names โ are always materialized on the stored document before it persists.
incoming values
โ set raw values (skip references until saved)
โผ
per-property permission + validation โโโ fail โโโบ return errors
โ
โผ
ProcessFormulaProperties (Jint) โ unique-key validation
โ
โผ
state transition (event type โ ObjectState)
โ
โผ
persist ExpandoObject to Mongo (insert or versioned update)
โ
โโโบ record ObjectHistory (CREATED / UPDATED / STATE_CHANGE)
โโโบ commit per-property history (old/new value + display)
โโโบ publish ObjectUpdateEvent / ObjectStateChangedEvent (OpenCqrs)
Each object type defines a state machine (Draft โ Pending Review โ Approved โฆ) keyed off event types. Every field change is captured as property history (raw and display values) and every record carries an object history trail โ the audit log is intrinsic to the engine, not a per-client feature. Saves and state changes emit domain events through the OpenCqrs dispatcher, which is how indexing, alerts, and workflow triggers hang off the model without coupling to it. System objects (sys_user, sys_role, sys_document) are themselves dynamic objects, so users and roles live in the same model as business data.
05 Permissions & the Workflow Engine
Access is role- and feature-based. Properties and objects carry permission sets checked on every read/write (Permissions.IsAllowed(AccessLevel.Edit)), and permissions can be scoped per object and per state โ e.g. a Reviewer may edit Reviewer Comments only while a record sits in Pending Review.
Workflows are first-class definitions bound to an object type. A WorkflowTemplate is composed of phases, each holding ordered steps that branch to a next phase based on outcomes, and a workflow gates entry with eligibility rules. Step types include Object State Change, Object Task (a human task with outcome conditions on object properties), Object Formula (an in-process Jint script that can create child records), plus Approval, Form, Email/Alert, Fulfillment Task, and Status. A request is a running instance of a workflow over a specific record, advancing through phases as steps complete.
06 The Client Setup Framework
A "client" โ an application built on WorkPath โ is authored as code that declares its application through the REST driver. The WorkPath.Clients project provides a family of abstract builders a client subclasses.
ObjectBuilder โ object templates, properties, choice lists,
validation, states, permissions, unique keys
WorkflowBuilder โ phases, steps, outcomes, contacts, eligibility
FormBuilder โ form layouts / property grouping ("tabs")
ViewBuilder โ list & detail views
MenuBuilder โ navigation
AccessBuilder โ roles & feature permissions
ObjectQueryBuilder โ saved queries
EmailBuilder ยท SettingsBuilder ยท TestDataBuilder
Each builder issues commands through the API driver, so a deployment is provisioned over HTTP against a running WorkPath instance rather than by direct DB seeding. Crucially, installs are versioned: a client ships Version1, Version2, Version3โฆ each a self-contained set of builders, and a ClientSetup.Install() applies them in order โ an explicit, replayable migration path. (Atleo's ProductLifeMgmt grew from formulas-and-ingredients in V1 to suppliers, a regulatory knowledge base, and live compliance assessments by V3 without the engine changing.)
07 Background, Search & Reflections
WorkPath.Runner is a Generic Host console app that executes platform work off the web request path: scheduled tasks (DynamicScheduledTask), fulfillment tasks kicked off by workflow steps (DynamicFulFillmentTask), and indexing (IndexDynamicObjectsAction) that pushes dynamic objects into Elasticsearch so the schemaless model stays fully searchable.
The whole platform is a bet that most enterprise software is the same shape โ typed records, relationships, validation, stateful approval flows, audit, search, notifications โ and that the only real variation is the schema and the rules. WorkPath pushes all of that variation into data and a thin per-client declaration layer, so building Atleo was a configuration exercise rather than a from-scratch project, and the next vertical would be too.
The costs are honest ones. A schemaless ExpandoObject model trades compile-time type safety for flexibility; correctness leans on the engine's validation and on tests. An embedded JavaScript layer is powerful but is logic that lives in strings rather than typed code. And authoring applications in C# builder classes โ versus a visual admin UI โ keeps provisioning developer-driven. For multi-client, regulated, workflow-centric systems where every customer's model differs, those trades have paid for themselves: the engine has stayed stable while the applications on top of it have moved fast.