Run a Close Task as a Module Automation
| Owner | Classification | Version | Effective | Next review | Status |
|---|---|---|---|---|---|
| Platform Engineering | Internal | 0.2 | 2026-09-05 | 2026-12-05 | Draft |
Purpose. Operating guide for how a CloseHub task step executes a real module engine instead of a hand-written result — the automation registry, the template binding that selects an engine, the parameter resolution that fills its input, and the dispatch to the owning module's worker. The companion Connected Close — GL Stitching covers how the same task resolves to the correct GL account across modules.
The chain (task step → automation → module engine)
A close task step does not compute its answer. It names an automation, the automation names a module workflow, and the orchestrator dispatches that workflow to the module that owns it.
closehub.tasktemplate ← the reusable close step (rollover source)
│
closehub.tasktemplateactivity (tta) ← one row per step activity
│ automationtaskid ─────────────► finbase.automationtask ← the REGISTRY
│ serviceid (legacy, Script Based) │ application (owning module)
│ │ tasktype / endpoint (the workflow)
▼ ▼
closehub.task (instantiated per period) orchestrator dispatch → MODULE worker
│ glaccountcode · segment1..10 · perioddate · businessentity
▼
closehub.taskactivityparameter {{tokens}} ← the step input, resolved from the task rowresolveStepForActivity reads tasktemplateactivity.automationtaskid. Because the binding lives on the template, every period's task inherits it — the change is rollover-safe and needs no per-period rework.
The registry — finbase.automationtask
Each row is one automation primitive, tagged with the owning module (application) and the workflow it runs (tasktype / endpoint).
| Application | Representative endpoints |
|---|---|
reconciliation | runaccountreconciliation, runfullreconciliation, autocertifyaccounts, setupreconciliation, transitionopenitems |
cash | cashapplication, runcollectionprocess, companyledgerageing |
invoice | calculateinvoiceageing, generatesupplierageingreport |
system (connectors/format) | fetchFileFromDrive, fetchFileFromSFTP, uploadFileToSFTP, csvToJson, pdfToXlsx |
closehub | importrollovertasks, rundailytaskautomations, generateexhaustivereport |
Two dispatch classes
tasktype | Meaning | Use |
|---|---|---|
A named workflow endpoint (e.g. runaccountreconciliation) | A job-tracked run dispatched through the orchestrator to the owning module's worker. | The correct pattern — the module engine produces an auditable result. |
Script Based (in-process SVC-* services) | An in-process step that writes its result directly. | Two cases. Result-asserting SVCs (asserted a status/difference, bypassing the engine) are deprecated and repointed onto workflow endpoints — e.g. SVC-MATCH-BANK → runaccountreconciliation. Lightweight import SVCs are retained: SVC-IMPORT-GL (82), SVC-IMPORT-SUBLEDGER (220), SVC-IMPORT-BANK (81) load a balance from a module or fixture without needing a tracked job. |
How a step's input is filled
Step parameters are authored once on the template with placeholders and resolved at run time from the running task's own row — no other table is read.
| Source | Mechanism |
|---|---|
| Scalar tokens | task-parameter.resolver substitutes , , , , etc. from the closehub.task row. A single-placeholder value passes its typed value through (a number stays a number); embedded placeholders interpolate as strings. |
| The reconciled account | attachReconPeriodSummaryId stamps periodSummaryId onto the step input by resolving glaccountcode (+ segment1) → reconciliation.account → periodsummary. See Connected Close — GL Stitching. |
An unknown token (author typo) throws at build time rather than reaching the engine as a literal — the downstream shell reports "completed" even on a failed flow, so silent failure is prevented by failing early. A known-but-null column resolves to "" and is reported back to the caller.
Declaring inputs for the config UI — finbase.automationparameter
The registry says which workflow runs; automationparameter declares that workflow's inputs, so the step-configuration dialog (FinAddStepDialog) renders a mapping form. One row per input, keyed to automationtaskid. The value column decides whether the input is developer-fixed or admin-facing.
value | Meaning in the config form |
|---|---|
A | Developer-fixed. Resolved from the task row at run time; not shown to the configurer. |
NULL | Admin-fillable. Rendered as a blank field for a genuine business choice. |
Example — Post Recon Adjustment (id 56): periodSummaryId and businessentity are fixed tokens; offsetAccount and offsetAccountName are NULL, so the configurer selects which account absorbs the reconciliation variance.
Where the automation layer is authored
Task structure and the automation layer come from different sources and are applied in sequence during onboarding. The Excel import owns structure; the automation layer is applied on top, exactly as fix-tasktemplate-phases.sql owns the phase layer.
| Layer | Source | Owns |
|---|---|---|
| Task structure | Excel template import (importtasktemplate) | tasks, activities, phases, dependencies |
| Automation binding + params | Blueprint SQL (wire-connected-close-automations.sql) | tta.automationtaskid / serviceid / actionurl + tasktemplateactivityparameter tokens |
| Config-UI declarations | declare-automation-parameters.sql | finbase.automationparameter (the input schema) |
The blueprint is idempotent and title-keyed (scoped to the tasktemplate title + activity), COALESCE-guarded so it never clobbers an existing binding. It writes to the template, never the instantiated task, and is re-run after every Excel re-import.
Two authoring paths — template vs user-defined
Automation reaches a step two ways, and the run-time one wins:
| Path | Where authored | Field | When |
|---|---|---|---|
| Template | Blueprint SQL / Excel (design time) | tasktemplateactivity.automationtaskid | Instantiated onto every period's task |
| User-defined | The UI, on a running task (FinAddStepDialog) | taskactivity.userdefinedautomationid (+ isuserdefinedtask=true) | Ad-hoc, one task |
The FinAddStepDialog surface lets a preparer add a step to a live task, toggle Automated, pick a Trigger Task from the finbase.automationtask registry, and supply the inputs that automation declares (finbase.automationparameter) — persisting userdefinedautomationid, automatedtaskname, and the parameter values.
Dispatch resolves the effective automation per step (taskrunner.automationIdForActivity):
userdefinedautomationid -- the step's OWN automation (user-added), wins
?? tasktemplateactivity.automationtaskid -- else the template's binding
(?? serviceid) -- else the legacy SVC pathSo a user-added step names its own automation and has no template behind it; a template-instantiated step falls back to its template's binding. Note this is the automation picker (a real recon-account/segment picker for glaccountcode does not exist yet — that binding is still free text; see Account Model & Chart of Accounts and GL Stitching).
Parameter rollover — template → instance
Both the binding and its parameters live on the template, so both must be copied onto each period's instance. Two code paths create instance activities, and each must copy parameters:
| Path | Copies parameters? |
|---|---|
First-run / lazy (ensureActivities) | Yes — reads tta.parameters, inserts taskactivityparameter. |
Bulk rollover (taskrollover_createactivities.razor) | PASS 2 copies activities; PASS 3 copies tasktemplateactivityparameter → taskactivityparameter (idempotent NOT EXISTS). |
Before PASS 3 existed, rolled-over automated steps inherited the engine binding but no parameters, so the resolver had nothing to fill. The instance column taskactivityparameter.tasktemplateactivityid references taskactivity(id) — the instance, not the template — despite the name.
Dispatch and tracking
- The step's resolved input is handed to the orchestrator.
- The orchestrator reads the automation's
endpointand starts the workflow on the owning module's worker (recon endpoints run on Recon's worker, cash on Cash's, connectors on the system worker). - The run is job-tracked: the trigger returns a job handle, not a result; the task polls
coresystem/v1/jobqueues/{id}to a terminalCompleted/Failedstate and records the outcome against the task activity.
The compose-a-chain pattern
Primitives sequence across a task's activities to automate an end-to-end control. A fully automated bank reconciliation is four registry endpoints in order:
Fetch File From SFTP → Csv To Json → bankbalanceimport → Run Account Reconciliation
pull the statement normalise load recon.bankbalance account clears (diff → 0)Worked example — Match Bank to GL
The "Bank Reconciliation - Operating Account" template's Match Bank to GL activity is bound to runaccountreconciliation (registry id 33, application reconciliation):
UPDATE closehub.tasktemplateactivity tta
SET automationtaskid = (SELECT id FROM finbase.automationtask
WHERE endpoint = 'runaccountreconciliation' LIMIT 1),
serviceid = NULL -- off the SVC-MATCH-BANK write-back
FROM closehub.tasktemplate tt
WHERE tt.id = tta.tasktemplateid
AND tt.title = 'Bank Reconciliation - Operating Account'
AND tta.activity = 'Match Bank to GL';At run time the step resolves periodSummaryId for the operating bank account and dispatches a real recon run on Recon's worker; the engine computes the account's unidentifieddifference, replacing the previous hand-written status.
Operational constraints
- The
wfwworker and Temporal must be running — every endpoint dispatch is an async job. Script Basedbindings are legacy; new automation binds a workflow endpoint.- The binding is template-level — change it on the
tasktemplate/tasktemplateactivity, not the instantiatedtask, so rollover preserves it. - The automation only runs against the correct account when the GL-account resolution succeeds — see the companion guide.
Related
- Connected Close — GL Stitching → how the step resolves to the right account across modules.
- Run the Reconciliation Matching Engine → the engine a recon step dispatches.
Revision history
| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-09-01 | Platform Engineering | Initial draft — automation registry, template binding, parameter resolution, dispatch, chain pattern. |
| 0.2 | 2026-09-05 | Platform Engineering | Added the config-UI input declaration layer (automationparameter, fixed vs admin-fillable), where the automation layer is authored (blueprint SQL vs Excel structure), and parameter rollover (template→instance, razor PASS 3). Refined the Script Based framing to separate deprecated result-asserting SVCs from retained lightweight-import SVCs. |
| 0.3 | 2026-09-17 | Platform Engineering | Added the two authoring paths — template vs user-defined (taskactivity.userdefinedautomationid via FinAddStepDialog, wins over the template binding); documented the dispatch precedence and noted the automation picker exists while the account/segment picker does not. |