Activation: browsing the catalog to a running intake
This page describes activation: how a customer goes from browsing the catalog to a running intake in their own tenant.
The honest constraint that shapes everything below:
A bundle carries structure. It cannot carry values.
The source layout, the field mapping, the transform, the target service or entity - all of that is the same in every tenant, and all of it ships. The SFTP endpoint, the credential, the journal name, the offset account, the bank account, the legal entity - none of that is the same in any two tenants, and none of it can ship. Baking those in is precisely what would stop an accelerator being an accelerator.
So activation is not "install the bundle". Activation is resolving the bundle's declared blanks against this tenant, in a defined order, with a check that nothing was missed before anyone touches an environment.
1. Where the design comes from
vendor-invoice-intake already modeled this, in miniature:
"batching": {
"defaults": { "journalNameId": "APInvoice" },
"provisioningRequired": ["offsetAccount", "journalNameId"]
}
That is the right idea - the bundle names what it refuses to ship - expressed as bare strings inside the batching block. It has three limits:
- It only reaches document-level values. It cannot describe an endpoint, a credential, or a target-side security grant, because those are not batching concerns.
- A bare string carries no type, no prompt, no default, no "this one is a secret".
- Nothing enforces it.
"offsetAccount"could be misspelled, or contradict the transform, and no tool would notice.
activation.parameters[] is that same idea, promoted and typed. batching.provisioningRequired
is retained and still works - and the validator requires that every entry in it has a matching
activation.parameters[].key, so the older field cannot drift away from the newer one.
2. What a bundle declares
"activation": {
"summary": "One line: what provisioning this bundle involves.",
"parameters": [
{
"key": "bankOffsetAccount",
"label": "Bank offset account",
"scope": "document",
"appliesTo": "request.Lines[].BankAccountId",
"dataType": "string",
"required": true,
"secret": false,
"default": null,
"prompt": "The bank account the cash lands in. Environment-specific, deliberately not shipped.",
"example": "BANK-USD-01"
}
],
"verify": "The smoke test that proves activation worked."
}
| Field | Why it exists |
|---|---|
key | camelCase, unique per bundle. Also the substitution token in the transform (§4). |
label | What a prompt or form field calls it. |
scope | Which provisioning step consumes it - see §3. |
appliesTo | The concrete field, contract member, or metadata element the value is written to. This is what makes the plan actionable rather than a wish list. |
dataType | string | number | boolean | date | secretRef |
required | Whether provisioning can complete without it. |
secret | Credential material: never logged, never written into a bundle artifact, resolved from Key Vault at run time. |
default | A suggested, overridable value. Absent means the bundle has no defensible default - which is itself information. |
prompt | What to ask, and why the bundle cannot answer it. The "why" is the part that stops a customer assuming it is a bug. |
allowedValues | A closed set, where one exists. |
The one rule that is enforced hard
A parameter that is secret: true (or dataType: secretRef) must not ship a default.
The validator fails the bundle otherwise. A default credential in a published catalog artifact is the single worst failure mode this format could have, so it is a structural error rather than a convention.
3. The four scopes
Scope is not decoration - it is the ordering key for the plan. Each scope is one provisioning step, and the steps have to happen in this order.
| Scope | Lands on | Example |
|---|---|---|
connection | A field on MGCLatticeFileIngestConnection | Endpoint, FolderPath, SftpHostKeyFingerprint, FixedWidthColumnSpec, AllowedServiceOperations |
secret | The connection's encrypted secret or a Key Vault reference - never the bundle | the SFTP credential |
target | Target-side wiring or grants | entity SubscriberAccessLevel, the OData client identity, dataAreaId |
document | Business defaults flowing into the request or record body | journalNameId, bankOffsetAccount, legalEntityId, requestIdSourceField |
4. The transform placeholder convention
A document-scoped value usually has to appear inside transform.liquid - the journal name and
the offset account are part of the request body, not part of the connection record. So the transform
marks those slots with a token:
"JournalNameId": "__journalNameId__",
"BankAccountId": "__bankOffsetAccount__",
"MethodOfPayment": "__methodOfPayment__",
Token format is __<parameterKey>__. It is deliberately not Liquid syntax: a Liquid variable
would silently render empty if nobody supplied it, whereas an unsubstituted __bankOffsetAccount__
fails loudly at the service boundary.
One parameter is substituted into a Liquid expression instead: requestIdSourceField names the
source field that holds a record's stable external id, so its slot is
"requestKey": "{{ __requestIdSourceField__ }}" and substituting orderReference yields
{{ orderReference }}. The service-invoke target derives the request's RequestId from that key
(§5). Left unsubstituted, the slot renders empty and RequestId falls back to one derived from the
ingested file and record, which still makes a retried record replay but not an order sent again in
another file.
This is machine-checked in both directions:
- Every
__token__in the transform must have a matchingactivation.parameters[].key- otherwise the bundle has a blank nobody is ever asked to fill. Validator error. - The catalog tool reports, per parameter, whether it has a transform slot, so
plancan print the exact substitution list (STEP 4).
A placeholder written in a comment counts as a placeholder too.
5. The activation flow
browse catalog/catalog.html (or registry.json)
| filter by vertical / target / format; each card now shows
| "activation: N value(s), M required"
v
resolve bundlecatalog resolve <id>
| validates, prints identity + loader artifacts + activation summary
v
plan bundlecatalog plan <id>
| ordered provisioning steps, grouped by scope, with the concrete
| connection fields and the exact placeholder substitutions
v
emit bundlecatalog plan <id> --emit-params values.json
| a fill-in-the-blanks file, pre-seeded with shipped defaults,
| plus every prompt printed to the console
v
[ customer fills values.json - the only manual step ]
|
v
check bundlecatalog plan <id> --params values.json
| missing required values, undeclared keys, and literal secrets that
| should be Key Vault references. Exit 1 until complete.
v
provision create the connection, store the secret, wire the target
| (customService: list the operation in Allowed service operations),
| substitute the transform, confirm document defaults
v
verify replay the shipped sample payload; activation.verify says what
success looks like for this specific bundle
Every step before provision is offline and side-effect free. That is intentional: the point of
--params is that a customer discovers they are missing a bank account before they start
creating connections, not halfway through.
One provisioning requirement of a customService bundle is not declared as a parameter, and
activation fails without it:
- Allowed service operations.
MGCLatticeFileIngestServiceInvokeTargetcalls only the operations listed in the connection's Allowed service operations field, and an empty list allows none: every record is then rejected with "The routing envelope names …, which is not in the connection's allowed service operations. Nothing was invoked." Enter the bundle's target asService.operation- the valueplanprints asAllowedServiceOperationsin STEP 3 (for exampleMGCLatticeVendorInvoiceJournalService.postInvoiceRegisterJournal).
The common request members the Lattice posting services validate need no provisioning beyond the
legalEntityId and requestIdSourceField parameters. The service-invoke target fills each of them
from the run context when the request leaves it empty, and a value the template writes always
wins:
RequestId- a name-based GUID of the connection, the legal entity, the operation and the envelope'srequestKey, so a record sent again (in any file) replays the first result instead of posting twice. Without arequestKey, it is derived from the ingested file and record, so a retried or replayed record replays. Authoring, section 4b gives the exact scheme.LegalEntityId- the company the processing batch job runs in. The shipped bundles write it fromlegalEntityId, so it is explicit.UserContext- the user the processing batch job runs as.CorrelationId- shared by all records of one file.
The File ingestion simulator, and the sample payload Replay that runs through it, never post.
Its last step previews the call: the request always gets a new random RequestId, whatever the
template or the requestKey say, so a simulation never shares an idempotency key with the
scheduled run. When the operation supports preview mode it is called in preview mode; otherwise
only the request is built and checked.
6. What the CLI does
tools/catalog (bundlecatalog) provides the plan command:
bundlecatalog plan <bundleId> [--root <path>]
[--emit-params <file>]
[--params <file>]
plan <id>- the ordered plan. Steps 1-6, each listing the parameters in that scope with theirappliesTotarget and any shipped default. Step 1 says what the record path means for the file format. Step 3 branches on target kind: acustomServicebundle gets the adapter wiring, including the Allowed service operations value the connection needs; anodataEntitybundle gets the collection plus the warning that the pipeline has no OData dispatcher (§8).plan <id> --emit-params <file>- writes the parameter template, pre-seeded with defaults, and prints every prompt.plan <id> --params <file>- completeness check. Exits1while any required value is blank. Warns on keys the bundle does not declare, and on asecretvalue that looks like a literal rather than a Key Vault reference.
resolve also reports the activation summary and points at plan.
registry.json and each catalog card carry
activation { declared, parameterCount, requiredCount, secretCount }, so a customer can see the
provisioning cost of a bundle before choosing it.
7. What was deliberately not built
- No UI. The design does not need one:
activation.parametersis a typed contract that a form, a wizard, or a deployment pipeline can all render. The CLI is a reference consumer, not the intended end-user surface. - No writes into F&O.
plannever creates a connection, stores a secret, or posts anything. It reads bundle artifacts and a local JSON file. Anything that mutates an environment is left to a human or a deployment pipeline, because the tooling has no authenticated channel into an AOS and should not grow one casually. - No secret handling. The tool checks that a secret looks like a Key Vault reference. It never reads, resolves, stores, or transmits secret material.
- No transform rewriting.
planprints the substitutions; it does not write a substituted template back to disk. Emitting a filled-in transform containing a tenant's bank account would create exactly the artifact this whole design exists to avoid.
8. Current limitations
Activation runs within these limits of the Lattice model:
- No provisioning page inside F&O. No D365 page reads a bundle, renders
activation.parametersas a prompt list and creates theMGCLatticeFileIngestConnection; provisioning is done by hand or by a deployment pipeline from theplanoutput. - No OData dispatcher in the ingestion pipeline.
MGCLatticeFileIngestDispatcherFactory::constructalways returnsMGCLatticeFileIngestCustomServiceDispatcher, and the only implementation ofMGCLatticeIFileIngestServiceTargetisMGCLatticeFileIngestServiceInvokeTarget, which invokes Lattice operations. So theodataEntitytarget style has no executable delivery path today. See the opportunity intake. - No subscriber write access on intake-target entities. No
AxDataEntityViewin the Lattice model grants subscriberCreateorUpdate: none of the 103 writable entities grants a subscriber more thanRead. EveryodataEntitybundle therefore carries a metadata-change prerequisite that a customer cannot perform at deploy time. - No record aggregation in the ingestion pipeline. See §9 -
batching.strategycurrently describes an intent the pipeline cannot honor. - Per-bundle gaps recorded in each manifest's
productDecisions: payment-journal settlement chaining, natural-key party resolution.
9. The granularity caveat every activation inherits
Worth stating here because it changes what a customer is actually provisioning.
The pipeline splits a file into one segment per record and renders the Liquid template once per segment:
MGCLatticeFileIngestPullcallsparser.parseRecords(content)and writes oneMGCLatticeFileIngestSegmentper returned record.MGCLatticeFileIngestSegmentServicerendersconnection.LiquidTemplateagainst that single segment'sRawContent.MGCLatticeFileIngestServiceInvokeTargetparses exactly one{ service, operation, request }envelope per rendered segment.
So one source record becomes one target document, and the Fluid model binds that record's
members at the top level ({{ customer_account }}) - there is no records collection in scope.
There is no aggregation step anywhere in the pipeline: every record becomes its own segment and its own service call. A batching.strategy of onePerFile therefore describes an
intention rather than current behavior, and a bundle should be authored so that one record already
is one document. See Authoring, section 4c.
10. Validation rules added
Enforced by tools/validator/BundleValidator.cs, so bundlevalidator and bundlecatalog agree:
| Rule | Severity |
|---|---|
activation.parameters present and an array, when activation is declared | error |
Each parameter has key, label, scope, dataType, boolean required | error |
key is camelCase and unique within the bundle | error |
scope ∈ connection | secret | target | document | error |
dataType ∈ string | number | boolean | date | secretRef | error |
A secret / secretRef parameter ships no default | error |
Every batching.provisioningRequired entry has a matching parameter key | error |
Every __token__ in the transform has a matching parameter key | error |