Auburn MyGov Docs Gitea

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 LayerDistribution & Publishing
PlatformMicrosoft Platform
ModuleReservations · 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

TriggerClassHow it firesAction 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 valueWhen 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
Multi-step approval pattern: To implement a staff-approval step, the on-submit actions create a notification to staff. When staff acts (approve/deny), a separate API call triggers 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.

Known schema inconsistency: The 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:

FieldV1 purposeV2 replacement
UserHtmlEmailIdFK to HtmlEmail template for staff notificationMailHandler with Config JSON template
ResponseHtmlEmailIdFK to HtmlEmail template for citizen confirmationMailHandler with Config JSON template
DataPushSprocStored procedure name for data pushMsSqlHandler or HttpHandler
DataProcessSprocStored procedure for data transform before actionMsSqlHandler
DataPushPathAPI endpoint for data pushHttpHandler

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).

ActionTypeHandler classPurposeV1/V2
LogicLogicHandlerConditional branching — if/else on context valuesV2
MailMailHandlerSend email via SMTP relay with Handlebars templateV2
MSGraphMSGraphHandlerMicrosoft Graph API operations: SendMail, SendTemplate, ProcessMailboxNDRs, and room booking actionsV2
HttpHttpHandlerGeneric HTTP request (GET/POST/PATCH) to external APIsV2
MsSqlMsSqlHandlerExecute SQL against the main database (stored proc or raw SQL)V2
SharePointSharePointHandlerRead/write SharePoint lists via Graph APIV2
AtlassianAtlassianHandlerCreate/update Jira issues and Confluence pagesV2
CityworksCityworksHandlerPush form responses to Cityworks work order systemV1/V2
CadIngestionCadIngestionHandlerIngest CAD (911 dispatch) dataV2
PushNotificationPushNotificationHandlerSend browser push notifications to subscribed staffV2
TeamsTeamsNotifyHandlerPost messages to Microsoft Teams channelsV2
NetworkConnectionNetworkConnectionHandlerNetwork connectivity utilitiesV2
DataDataHandlerContext data manipulation — set/copy/transform valuesV2
IISLogParseIISLogParseHandlerParse and ingest IIS access log filesV2
VectorizeContentVectorizeContentHandlerGenerate vector embeddings for content (AI search)V2
ArcGisArcGisHandlerQuery ArcGIS REST API for spatial dataV2
MunisMunisHandlerIntegration with Tyler Munis ERPV2
WebContentPublishWebContentPublishHandlerPublish web content recordsV2
CitySourcedCitysourcedHandlerCitySourced citizen reporting integrationV1/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) and FormId.
  • 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 = null actions (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-editor web component with foreignKey=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

FeatureStatusNotes
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.