Workflow engine licence monitor
Scope. How the Workflow engine counts and lists Designated Workflows, reads a licence record offline, reports a licence status with its warnings and shows the licence notice. For operators who run the engine and for implementers who host it. The file format is the component licence file contract.
The licence status is informational. The engine refuses, restricts and stops nothing because of a count, a notice level, a licence record or its status: every workflow registered in code or imported is registered, recorded and served, whatever the count (Thresholds and warnings). The status never fails readiness or liveness and never triggers a restart. The engine makes no network call and sends no telemetry: the count, the licence record and the status stay in the installation.
The status describes the licence records the local reader can see. It is not a determination of entitlement or compliance; the order and the applicable licences prevail over the file and over any status. The absence of a warning is not a compliance attestation.
Counting
The counted unit is the Designated Workflow: every workflow the installation has ever registered in code (the code catalogue, IWorkflowRegistrationCatalog) or stored in the definition store (definition import). The count is cumulative:
- A workflow counts from the first time the engine sees it and keeps counting. Removing it from the code catalogue, replacing its versions or activating another version does not reduce the count. The product has no operation that removes a workflow from the count.
- Versions are not counted: a workflow with several versions counts once.
- Names that differ only in letter case are one workflow, as the engine resolves them.
The engine counts with a technical counter: the workflow names recorded in the first-seen ledger. It is separate from the contractual Designated Workflow count of the component licence. It includes names registered only for non-production use and counts names, not logical workflows: a new name adds a unit even when it continues an earlier workflow, and distinct workflows that share a name are one unit. Its rules:
- The counter is cumulative within its ledger: a recorded name is never removed. The durable ledger keeps its names across restarts; the process-memory ledger keeps them only while the process runs.
- A unit is added only when a name is recorded, by registration from the code catalogue or by storage through an import.
- An operation that fails before its name is recorded adds nothing.
- Further versions under a recorded name add nothing.
- Names compare case-insensitively. There is no alias mechanism: each distinct name is a unit.
- Deleting and registering again, or restoring, a workflow whose name is recorded adds nothing, because the ledger is insert-only.
- Engines that share one ledger are serialised, so each name is recorded once. A host without the shared ledger applies the same rule in memory, per process.
Losing or replacing the ledger does not reset the installation’s contractual count.
The count is kept in the first-seen ledger:
| Host | Ledger | Behaviour |
|---|---|---|
PostgreSQL store (WorkflowBackend:Provider = Postgres) | Table workflow.wf_first_seen_workflows, created with the store’s schema on startup | Insert-only and durable. One row per workflow with its first sighting (catalog, store or import) and time. The table refuses UPDATE, DELETE and TRUNCATE. |
Oracle store (WorkflowBackend:Provider = Oracle) | Table WF_FIRST_SEEN_WORKFLOWS in the workflow schema, created by install-clean.sql | Insert-only and durable, with the same rows. The table refuses UPDATE and DELETE. Running install-clean.sql again keeps the table and its rows. |
MongoDB store (WorkflowBackend:Provider = Mongo) | Collection workflow_first_seen_workflows (WorkflowBackend:Mongo:FirstSeenWorkflowsCollectionName), created by the store’s initialization with a unique index on the upper-case name | Insert-only and durable, with one document per workflow and the same fields. The store has no update or delete path; MongoDB itself does not refuse a change made outside the store. |
| A host without a workflow store | Process memory | Counts what the running process has seen: the code catalogue and, where the host provides its own definition store, the stored workflows and the imports since startup. The count starts again at each restart. |
On a host without a workflow store the count can therefore be incomplete. The health entry reports the ledger as process-memory in that case, and durable on the PostgreSQL, Oracle and MongoDB stores.
The ledger is filled in two places:
- Licence evaluation, at startup and every hour: the engine records every workflow of the code catalogue, then every workflow stored in the definition store (active or not). The second pass also records definitions that were stored before the ledger existed, and imports whose recording failed.
- Definition import (
WorkflowDefinitionDeploymentService.ImportAsync): a stored workflow is recorded after it is stored. An import that fails before it is stored records nothing. If the ledger cannot be written, the import still succeeds, a warning with thecount-unavailablemessage is logged, and the next evaluation records the workflow from the definition store.
On PostgreSQL and Oracle every recording locks the ledger table for its transaction, so engine instances that share the database record each name once. The lock only orders the recordings; it refuses nothing. On MongoDB the unique index keeps one document per name: of concurrent first sightings, the first one written is kept, and the others find the name recorded.
The definition stores (PostgreSQL, MongoDB, and the optional Oracle adapter) delete no definition; activating another version keeps the earlier ones. A stored workflow therefore stays in the count for the life of the store.
Thresholds and warnings
The component licence sets two levels for the count, ComponentFreeLimits.WorkflowNoticeLevel and ComponentFreeLimits.WorkflowFreeLimit (StellaOps.Components.Licensing). The component licence states their values.
- Up to the notice level the status is
within-free-limit. It is logged at information level at startup and when it changes. - Above the notice level and within the free limit, without a licence entry that covers the running version, the status is
above-notice-level: a warning with counsel’s message, which says that the technical count is not the contractual Designated Workflow count. Crossing the notice level does not itself establish that additional production permission is required and creates no fee. - Above the free limit the status depends on the licence records:
record-matcheswhen a standard-capacity entry covers the running version and the count is within its capacity,count-exceeds-recorded-capacitywhen the count is above that capacity,no-matching-recordwhen no entry covers the running version, anddevelopment-buildwhen the running build carries no first-publication date.
Every status except within-free-limit and record-matches is a warning in the log at startup and every hour (Output). A warning refuses nothing: imports and code registrations are never refused for licence reasons, every workflow keeps running and takes new versions, and readiness and liveness never change. Every installation counts, in every environment; no setting turns the count or the warnings off. A development build carries no first-publication date, so no standard-capacity entry covers it.
When the ledger cannot be read, the status is count-unavailable with the message “The local workflow-name count is unavailable.” The engine keeps serving every workflow and accepts imports; the next evaluation reads the ledger again.
A status withdraws no right; the order and the applicable licences prevail over the licence record and over any status or warning.
Listing the count
GET /api/workflow/designated-workflows lists the first-seen ledger and returns its count. It needs the View policy (scope workflow:view, workflow:operate or workflow:admin). It only reads: it records nothing and sends nothing anywhere. It also returns the licence status and the licence notice of the latest licence evaluation.
curl -s https://workflow.example/api/workflow/designated-workflows -H "Authorization: Bearer $TOKEN"
{
"designatedWorkflows": 3,
"ledger": "durable",
"workflows": [
{ "workflowName": "SampleImported", "firstSeenSource": "import", "firstSeenOnUtc": "2027-06-01T08:05:00+00:00" },
{ "workflowName": "SampleRetired", "firstSeenSource": "catalog", "firstSeenOnUtc": "2027-06-01T08:00:00+00:00" },
{ "workflowName": "SimpleApproval", "firstSeenSource": "catalog", "firstSeenOnUtc": "2027-06-01T08:00:00+00:00" }
],
"licence": {
"status": "within-free-limit",
"message": "Recorded workflow-name count: 3.",
"explanation": "The notice levels are informational and are below the standard free-production limits of [free limit] Endpoints for Router and [free limit] Designated Workflows for Workflow. Crossing a notice level does not itself establish that additional production permission is required and does not create a fee. The applicable LICENSE, any Order and other independently applicable permissions determine permitted use.",
"warns": false
},
"notice": {
"text": "Licensed to Northwind Traders | component: workflow | licensed capacity: 1000 | covers versions first published before: 2027-11-03 | status: within-free-limit",
"case": "licensed-to",
"licensee": "Northwind Traders",
"permission": "standard-capacity",
"licensedCapacity": 1000,
"coversVersionsPublishedBefore": "2027-11-03",
"versionScope": null,
"effectiveDate": null,
"recordsInForce": 1,
"licenceFilesNotInForce": 0
}
}
| Field | Content |
|---|---|
designatedWorkflows | The number of workflows in the ledger, which is the number of entries in workflows. It is the count the monitor evaluates. |
ledger | durable on the PostgreSQL, Oracle and MongoDB stores; process-memory on a host without a workflow store. |
workflows | One entry per workflow, ordered by the upper-case name: the spelling of its first sighting, where it was first seen (catalog, store or import) and when (UTC). A workflow removed from the code catalogue keeps its entry. |
licence | The licence status of the latest licence evaluation: status, its factual message, the licence-status explanation and warns (true for a status logged as a warning). null before the first evaluation of the running process. |
notice | The licence notice of the latest licence evaluation, or null before the first evaluation of the running process. |
The list shows the ledger as its latest recording left it. An import is listed at once, while the monitor’s designatedWorkflows (the licence entry of /readyz) changes at its next evaluation, at most an hour later; after that evaluation both report the same count. If the ledger cannot be read, the request fails; the endpoint never returns a partial list.
Count procedure
On a host with the PostgreSQL, Oracle or MongoDB store (ledger is durable):
- Read
GET /api/workflow/designated-workflows. - The count is
designatedWorkflows. It includes every workflow ever registered in code or stored, also those removed from the code catalogue since. - To check it against the monitor, compare it with
designatedWorkflowsin thelicenceentry of/readyzafter the next evaluation, or after a restart.
On a host without a workflow store (ledger is process-memory) the endpoint lists only the workflows registered in code in the running process, so a workflow removed from the code catalogue in an earlier deployment is missing. Keep a local register instead:
- Opening register. When counting starts on the installation, record every workflow the endpoint lists, with the date.
- Later designations. After every deployment, read the endpoint again and add each workflow the register does not hold, with the date.
- No removals. Never remove an entry. A workflow removed from the code catalogue stays in the register, as it would stay in the durable ledger.
- The count is the number of entries. Names that differ only in letter case are one entry, and versions are not counted.
The register stays in the installation and is sent nowhere.
Covered versions
A licence file entry covers the versions of the engine first published before its date (coversVersionsPublishedBefore, an exclusive UTC calendar date). The engine takes the first-publication date of the running version from the StellaOps.Component.PublishedAt assembly metadata of the engine assembly, stamped when the release is packed. A build or adaptation of a released version keeps the stamp of its source release; its own build date is never used. A build without the stamp is a development build.
The current date never changes the result: a version first published before an entry’s date stays covered after that date has passed. A standard-capacity entry records its capacity as a number of Designated Workflows; the highest capacity among the entries that cover the running version applies, and entries are never added together.
An embedded line records a no-fee permission for software a customer delivers to its own clients: one entry with unlimited capacity, the version scope delivered-without-cutoff (any version the customer delivers) and an effective date, without a cutoff. It covers every version, a development build included. The engine cannot verify the named product, the end-customer installation or the delivery, so above the notice level the status is embedded-scope-not-established, and the notice describes the recorded permission without certifying it.
The licence file
The licence record is a DSSE envelope signed with a Stella Ops vendor-entitlement key and checked offline, supplied as a file or as a licence key (the same signed record as one base64 line). Use within the free limit needs no record. A record that fails a check is not loaded and its reason is shown as a detail; it never stops the engine.
| Configuration key | Purpose |
|---|---|
StellaOps:ComponentLicence:Path | A licence file, or a directory whose *.dsse.json files are read (at most 64). Local path only. Optional. |
StellaOps:ComponentLicence:Key | The licence key: the standard base64 of a licence file, as one line. Base64 is an encoding, not a protection. Optional. Set the path or the key: with both set, neither is read and the details show conflicting_sources. |
StellaOps:ComponentLicence:InstallationId | The Installation identifier the file names, a lowercase UUID. A file for another identifier, or a file read while no identifier is configured, is not loaded (wrong_installation). |
STELLAOPS_VENDOR_TRUST_ROOTS (environment) | Optional directory with a vendor trust-root set that replaces the embedded published root, for key rotation. |
The engine reads the records again at every evaluation, so a new file takes effect within an hour without a restart. The refusal codes and the continuity rules of later revisions are in the contract.
Statuses
The first status that holds is reported. [count] is the technical count, [notice level] the notice level, [date] the cutoff of the deciding entry.
| Status | When | Message |
|---|---|---|
count-unavailable | The ledger cannot be read. | The local workflow-name count is unavailable. |
within-free-limit | The count is at or below the notice level. No record is needed. | Recorded workflow-name count: [count]. |
embedded-scope-not-established | The count is above the notice level, and an embedded entry decides. | The local record does not establish that this use is within its named product and end-customer Installation scope. Check the applicable Order and any other permission. |
record-matches | The count is above the notice level, a standard-capacity entry covers the running version, and the count is within its capacity or the free limit. | This record covers versions first published before [date]. |
count-exceeds-recorded-capacity | The count is above the free limit and above the capacity of the standard-capacity entry that decides. | The local assessment could not confirm recorded capacity for this version and count. Check the applicable LICENSE, Order and any other permission. |
above-notice-level | The count is above the notice level and within the free limit, and no entry covers the running version. | Recorded workflow-name count: [count]. This is above the [notice level]-name notice level. This technical count is not the contractual Designated Workflow count. |
development-build | The count is above the free limit, no entry covers the running version, and the running build carries no first-publication date. The details add: This build carries no first-publication date, so the licence records are not assessed. | The local assessment could not confirm recorded capacity for this version and count. Check the applicable LICENSE, Order and any other permission. |
no-matching-record | The count is above the free limit, and no loaded entry covers the running version. | The local assessment could not confirm recorded capacity for this version and count. Check the applicable LICENSE, Order and any other permission. |
The licence-status explanation accompanies the messages in the warning log, the health entry and the designated-workflows API:
The notice levels are informational and are below the standard free-production limits of [free limit] Endpoints for Router and [free limit] Designated Workflows for Workflow. Crossing a notice level does not itself establish that additional production permission is required and does not create a fee. The applicable LICENSE, any Order and other independently applicable permissions determine permitted use.
No status or message says that an installation is unlicensed, non-compliant or expired.
Licence notice
The engine shows a licence notice built from each licence evaluation. When an entry of a loaded record covers the running version, the notice is “Licensed to” the licensee display name of the line whose entry decides; otherwise it is the message of the case:
case | Notice |
|---|---|
licensed-to | Licensed to [licensee] |
no-licence-file | No local licence record is loaded. See the applicable LICENSE and Order for permitted use. |
no-file-loaded | The local licence record could not be authenticated or matched to this Installation. |
records-not-assessed | This build carries no first-publication date, so the licence records are not assessed. |
no-covering-record | This record covers versions first published before [date]. It does not record paid coverage for this version. |
“Licensed to [licensee]” identifies the local licence record. It does not certify the author, content, authenticity or compliance of a workflow, diagram or other output. For an embedded line the notice describes the recorded permission (permission, unlimited capacity, version scope and effective date) and does not certify that the use is within it.
The contract states the cases and the one-line form. In the health and API output the notice is an object with the fields and case names of the Router’s licence notice, and the fields of an embedded line:
| Field | Content |
|---|---|
text | The one-line form, as the log line writes it. |
case | One of the case names above. |
licensee | The licensee display name of the deciding line; null unless licensed-to. |
permission | standard-capacity or embedded; null unless licensed-to. |
licensedCapacity | The capacity of the deciding entry, in Designated Workflows; null for an embedded line, which is unlimited, and unless licensed-to. |
coversVersionsPublishedBefore | The date of the deciding standard-capacity entry (YYYY-MM-DD); for no-covering-record the latest date of the loaded entries; otherwise null. |
versionScope | delivered-without-cutoff for an embedded line; otherwise null. |
effectiveDate | The effective date of an embedded line (YYYY-MM-DD); otherwise null. |
recordsInForce | The licence records in force. |
licenceFilesNotInForce | The licence records that were read and are not in force. |
The notice appears in four places:
- Logs: one information line at startup and one at every hourly evaluation, whatever the status, in the one-line form, for example
Workflow licence notice: Licensed to Northwind Traders | component: workflow | licensed capacity: 1000 | covers versions first published before: 2027-11-03 | status: record-matches. - Health: the
noticeobject in the data of thelicenceentry. The retained web host serves/readyzwithout authentication, so the licensee display name is visible to anyone who can reach that endpoint; licence serials, order references and the Installation identifier are not in it./healthzreports no entry data. - Licence API: the
noticeofGET /api/workflow/designated-workflows, which needs the View policy. - SVG renders:
WorkflowRenderSvgRenderer.Render(layout, title, licenceNotice)(StellaOps.Workflow.Renderer.Svg) writes the notice as visible text along the bottom edge of every diagram (attributedata-licence-notice), escaped for XML. A host that runs the engine passesNoticeHeadlineof the latest evaluation (WorkflowLicenceState.Current); a host that loads no licence record, such as a build-time export, passesWorkflowRenderSvgRenderer.NoLicenceRecordNotice. An SVG stored with an imported definition is served as it was imported.
Output
| Output | Content |
|---|---|
| Log | One warning at startup and one every hour while the status warns: every status except within-free-limit and record-matches. It gives the status, the message, the recorded workflow names (unavailable when the ledger cannot be read), the notice level, the free limit, the ledger kind, the number of records loaded and not loaded, the licence-status explanation and the details (records not loaded with their reasons, the scope of every loaded entry). within-free-limit and record-matches are logged at information level at startup and when the status changes. The licence notice is logged at information level at startup and every hour. A ledger that cannot be read at an evaluation or an import is logged as a warning with the count-unavailable message. |
| Health | The licence entry of the health report. It always reports Healthy, so it never changes readiness or liveness. Its data holds status, message, explanation, warns, designatedWorkflows (the technical count; null when the ledger cannot be read), noticeLevel, freeLimit, ledger, publishedAt (none in a development build), licenceFilesLoaded, licenceFilesNotLoaded, installationIdConfigured, evaluatedAt, informational and the notice object. The retained web host writes its /readyz report as JSON with each entry’s data; another ASP.NET Core host gets the same report by using WorkflowHealthResponseWriter.WriteAsync (StellaOps.Workflow.AspNetCore) as the response writer of its readiness endpoint. |
| Metric | The gauge stellaops.workflow.licence.designated_workflows (meter StellaOps.Workflow.Licence): the technical count of the latest evaluation, tagged licence.status with the status, and no measurement while the count is unavailable. It reaches only the metrics pipeline the operator configures. |
The monitor (WorkflowLicenceMonitor) and the health entry are registered by AddWorkflowEngineHostedServices; the ledger, the licence record reader and the evaluation by AddWorkflowEngineCoreServices, and the durable ledger by AddWorkflowPostgresDataStore. The first evaluation runs once the host has started, after the store has created its schema.
