๐Ÿฆ‰ ~/projects/workpath — pwsh

PS andrewfry.dev> cat /projects/workpath.md

cd .. to return to /projects

WorkPath

Platform ยท Low-Code Engine โ€” the metadata-driven platform that Atleo and others are declared on top of

STATUS: ACTIVE SOLUTION: ~61_PROJECTS MODEL: SCHEMA_AS_DATA
[C#][.NET Core 3.1][MongoDB][Elasticsearch][Jint][OpenCqrs][Blazor]
  • ~61 projects in one solution
  • 27 dynamic property types
  • 4 layered tiers
  • 1 engine ยท many apps

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.

fig 1 โ€” the four tiers
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  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.

fig 2 โ€” engine-managed system fields
  __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.

fig 3 โ€” ProcessAndSave
  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.

fig 4 โ€” the declarative builders
  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.

> next: cat /projects/gaf โ€” the GAF feature pipeline