Platform
Workflow Engine
Config-driven automation engine — triggers, handler library, context model, and what's needed to support reservation approval flows.
| Status | In Progress |
| Last updated | — |
| Related |
Layer Distribution & Publishing Platform Microsoft Platform Module Reservations · Programs & Registration · Forms |
Overview
The Workflow Engine (internally called the ServerJob / WorkflowActionRunner system) is the automation glue across MyGov. When a form is submitted, a webhook fires, or a cron schedule fires, the engine executes a sequence of configured actions in order. Each action is a call to a registered handler with a JSON config.
The engine does not maintain persistent state between runs — it is not a BPM (business process management) system. Each run is synchronous and stateless. Handlers share a Dictionary<string, object?> context that lives for the duration of one run only.
Trigger Types
| Trigger | Class | How it fires | Action source |
|---|---|---|---|
| Form Response | WorkflowTrigger.FormResponse |
Citizen or staff submits a form. FormResponsesController calls WorkflowActionRunner.StartWorkflow(FormResponse, schedule?). |
organization.FormAction rows where FormId matches and Schedule matches the trigger. |
| Webhook Event | WorkflowTrigger.WebhookEvent |
External system POSTs to an inbound webhook endpoint. WebhookAction rows fire in order. | organization.WebhookAction rows where WebhookId matches. |
| Server Job (Cron) | WorkflowTrigger.ServerJob |
IHostedService checks it.ServerJob rows on a tick. Jobs with a matching cron expression run their ServerJobAction rows. |
it.ServerJobAction rows for that ServerJob. |
FormAction schedule values
The Schedule column on organization.FormAction controls when an action fires within the form-response trigger:
| Schedule value | When it runs |
|---|---|
null or "Trigger" | Immediately on form submit (the default path) |
"Status:New" | Same as Trigger (legacy compatibility) |
Any other string (e.g. "Status:Approved") | Only when StartWorkflow(formResponse, "Status:Approved") is called explicitly — used for multi-step approval flows |
StartWorkflow(formResponse, "Status:Approved") or "Status:Denied", running the corresponding action set. This is how reservation approval would work — but there is no built-in "wait for staff input" pause state. The engine runs to completion on each trigger.
Action Config Structure (V2)
Each action row has an ActionType string (handler name) and a Config JSON field:
{
"Op": {
"Action": "SendMail", // handler-specific operation name
"Source": "context.path", // optional input from context
"Target": "recipient@..." // optional target (mailbox, endpoint, etc.)
},
"Data": {
// handler-specific payload — may be a template string or an object
"Subject": "New reservation request",
"Body": "Hello {{FirstName}}, ..."
}
}
Handlebars templating ({{path.to.value}}) is supported in string fields. Values are resolved from the workflow context dictionary.
Data field serves dual purpose — in some handlers it is a template string (the email body), in others it is a structured object. This inconsistency is a known issue in the codebase. When reading or writing workflow configs, check the specific handler docs.
V1 vs V2 Architecture
FormAction has legacy "V1" fields from before the Config JSON approach:
| Field | V1 purpose | V2 replacement |
|---|---|---|
UserHtmlEmailId | FK to HtmlEmail template for staff notification | MailHandler with Config JSON template |
ResponseHtmlEmailId | FK to HtmlEmail template for citizen confirmation | MailHandler with Config JSON template |
DataPushSproc | Stored procedure name for data push | MsSqlHandler or HttpHandler |
DataProcessSproc | Stored procedure for data transform before action | MsSqlHandler |
DataPushPath | API endpoint for data push | HttpHandler |
New workflows should use V2 (Config JSON only). V1 fields are retained for backward compatibility but not recommended for new work.
Handler Library
All handlers implement IActionHandler and are registered in DI. The ActionHandlerFactory resolves by ActionType string (case-insensitive).
| ActionType | Handler class | Purpose | V1/V2 |
|---|---|---|---|
Logic | LogicHandler | Conditional branching — if/else on context values | V2 |
Mail | MailHandler | Send email via SMTP relay with Handlebars template | V2 |
MSGraph | MSGraphHandler | Microsoft Graph API operations: SendMail, SendTemplate, ProcessMailboxNDRs, and room booking actions | V2 |
Http | HttpHandler | Generic HTTP request (GET/POST/PATCH) to external APIs | V2 |
MsSql | MsSqlHandler | Execute SQL against the main database (stored proc or raw SQL) | V2 |
SharePoint | SharePointHandler | Read/write SharePoint lists via Graph API | V2 |
Atlassian | AtlassianHandler | Create/update Jira issues and Confluence pages | V2 |
Cityworks | CityworksHandler | Push form responses to Cityworks work order system | V1/V2 |
CadIngestion | CadIngestionHandler | Ingest CAD (911 dispatch) data | V2 |
PushNotification | PushNotificationHandler | Send browser push notifications to subscribed staff | V2 |
Teams | TeamsNotifyHandler | Post messages to Microsoft Teams channels | V2 |
NetworkConnection | NetworkConnectionHandler | Network connectivity utilities | V2 |
Data | DataHandler | Context data manipulation — set/copy/transform values | V2 |
IISLogParse | IISLogParseHandler | Parse and ingest IIS access log files | V2 |
VectorizeContent | VectorizeContentHandler | Generate vector embeddings for content (AI search) | V2 |
ArcGis | ArcGisHandler | Query ArcGIS REST API for spatial data | V2 |
Munis | MunisHandler | Integration with Tyler Munis ERP | V2 |
WebContentPublish | WebContentPublishHandler | Publish web content records | V2 |
CitySourced | CitysourcedHandler | CitySourced citizen reporting integration | V1/V2 |
Workflow Context
The context is a Dictionary<string, object?> (WorkflowActionContext) scoped to a single run. It is initialized from the trigger payload (form response JSON, webhook payload JSON, or server job config). Handlers can read from and write to the context to pass data between steps.
- Form response trigger: context includes all form field values (keyed by field name), plus
Id(response ID) andFormId. - Webhook trigger: context is initialized from the raw webhook payload JSON.
- Server job trigger: context starts empty or with job config values.
Handlebars paths like {{FieldName}} or {{nested.property}} are resolved against this context when processing Config templates.
What's Needed for Reservation Approval
The current engine can support a basic reservation workflow, but two gaps exist relative to the design in Reservations:
1. Async pause state
The engine has no built-in "wait for staff approval" state. The multi-step pattern (described above in Trigger Types) requires the consuming code to explicitly re-trigger the workflow for each state change. For reservations, this means:
- On submit: run
Schedule = nullactions (notify staff, set status to Pending) - When staff approves: call
StartWorkflow(formResponse, "Status:Approved")from the approval endpoint - When staff denies: call
StartWorkflow(formResponse, "Status:Denied")
This is buildable with the current engine — the pattern just needs to be wired into the reservation controller.
2. Reservation-specific action handlers
The design calls for new workflow actions specific to reservations:
- CreateCalendarEntry — write a CalendarEvent for the confirmed reservation
- BlockExchangeRoom — write to Exchange resource mailbox via MSGraph (partially implemented in MSGraphHandler)
- SendCancellationLink — generate HMAC-signed cancellation token and email it (could use MailHandler with template)
- CheckConflict — query availability before confirming (would be a new MsSql or custom handler action)
- ValidateLibraryCard — patron ID lookup (new Http or MsSql action to an external API)
Most of these can be composed from existing handlers (MSGraph, Mail, MsSql, Http) with the right Config. The library card lookup needs a new Http or custom handler target.
Admin Portal — Forms Module
The my/js/modules/forms/ module manages forms and their workflow actions. The key page:
- FormActionsPage.js — renders the
workflow-action-editorweb component withforeignKey=formId,foreignKeyName='formId',createPath='/form/action',timingMode='schedule'. This is the UI for configuring which actions run for a form and when.
The workflow-action-editor web component is a shared component used across forms, webhooks, and server jobs — it knows how to render the Config JSON editor for each handler type.
Audit Findings
Codebase audited: api/Classes/Workflow/, api/Classes/Workflow/Handlers/, api/Models/organization/FormAction.cs, api/Controllers/forms/FormActionsController.cs, my/js/modules/forms/pages/FormActionsPage.js
| Feature | Status | Notes |
|---|---|---|
| Core engine (trigger → actions → context) | BUILT | All three trigger types implemented; synchronous sequential execution |
| Handlebars templating | BUILT | TemplateHelper resolves context paths in Config string fields |
| Handler registry (19 handlers) | BUILT | All listed handlers registered; MSGraph, Mail, Http, MsSql, Logic are the most relevant for cross-module integration |
| Multi-step approval via schedule values | PARTIAL | Mechanism exists (schedule string matching); not wired for reservations yet |
| MSGraph NDR processing action | BUILT | ProcessMailboxNDRs implemented and active — ran successfully on first live campaign |
| Async/paused state (engine-native) | NOT BUILT | Engine is synchronous per-run. No built-in pause/resume. Must be simulated via explicit re-trigger calls from the reservation controller. |
| Reservation-specific action handlers | NOT STARTED | CreateCalendarEntry, BlockExchangeRoom (partially in MSGraph), CheckConflict, ValidateLibraryCard, SendCancellationLink all need implementation |
| V1 field retirement | INVESTIGATE | UserHtmlEmailId, ResponseHtmlEmailId, DataPushSproc, DataProcessSproc, DataPushPath still in FormAction schema. Audit whether any active FormAction records still populate V1 fields — if none do, column removal can proceed immediately. If any remain, migrate those configs to V2 Config JSON first, then drop the columns. |
| Data field dual-purpose inconsistency | KNOWN GAP | Documented as a known issue; needs a schema decision (separate TemplateBody field, or convention in handler docs) |
| ArcGisHandler — actions not audited | NOT AUDITED | Handler file exists but was not read in this audit pass. Audit what spatial query actions are exposed before building any location-dependent workflow steps. |
| MsSqlHandler — CredentialId enhancement | PLANNED | Handler already restricted to stored procedures and parameterized queries — developer-only config, no injection risk. Planned enhancement: associate MsSqlHandler action configs with a CredentialId so the handler can target database contexts beyond the main application database (e.g. a legacy system or reporting DB). |
| Polaris ILS handler — future investigation | INVESTIGATE | Library card validation is currently handled by a custom built-in mechanism. Worth investigating a dedicated PolarisHandler that interfaces with the Polaris ILS API directly — mirroring the CityworksHandler pattern: HTTP-based, structured action calls, isolated handler class. Keeps security model consistent and avoids bespoke validation logic scattered through the codebase. |