Agent skill

dlake-crmpro

When an agent should use it: Operate **CRMPro**, Commercient's forward sync agent, through the `dlake` CLI (npm `@commercient/dlake`) — remotely, with the `crmpro_*` admin tools (`dlake admin crmpro_list_processes`, `crmpro_get_process`, `crmpro_update_process`, `crmpro_set_sync_enabled`, `crmpro_field_mapping`, `crmpro_sync_history`, `crmpro_errors`, `crmpro_flags`, `crmpro_templates` and the rest): list and edit sync processes, enable or disable sync, map fields, apply templates and diagnose a run from anywhere. It also covers the underlying tables when a tool does not reach them — `CRM_Configuration`, `CRM_FieldList`, the `CommercientFlags` flag table, `CRM_Parameters`, the Connection Manager records, and the per-run tables `TimeStampRepository`, `CRMProRunHistory`, `CRMPRO_DASHBOARD_ERROR`, `CRMPRO_ERROR_LOG`, `CRMPRO_DeleteRecordInfo`, `CRMBackupObjectList`. Use this skill whenever the task mentions CRMPro, a `crmpro_` tool, the forward/ERP-to-CRM leg, a sync process or config row, a `Sync_Order` / `Sync_Operation_Type` / `TimeStamp_Prefix` value, a CRMPro flag, or field mapping through the JSON- and XML-bearing columns. For the writeback leg use `dlake-txdownloaderpro`; for standing an integration up use `dlake-integration-setup`; for general tenant operation use `dlake`.

Install with "dlake skills install" or read it in the terminal with "dlake skills show dlake-crmpro". Skills ship inside the CLI; the copy below mirrors the GitHub repository.

Operating CRMPro with dlake

Keep this skill current. dlake ships updates often and this text is embedded in the CLI you have installed. Before relying on it, compare dlake --version with npm view @commercient/dlake version; if they differ, run npm install -g @commercient/dlake@latest and then dlake skills install, which overwrites the installed skill files with the current text.

CRMPro is Commercient’s forward sync engine. It reads a customer’s ERP data out of their gateway database and pushes it into the CRM or e-commerce platform, covering the whole lifecycle: schema creation in the destination, incremental change detection by timestamp, transformation, destination API calls, error logging and summary reporting. It runs per customer on a schedule, and supports 16+ destination platforms (CRM and e-commerce alike).

Its counterpart is TxDownloaderPro, which runs the other way — CRM back toward the source. They are different products. Write both names in full, always; abbreviating either points at the wrong thing, and their tables sit side by side in the same database.

CRMPro is flag-driven. Runtime behaviour comes from the flag table in the database rather than from static configuration files. That is the single most important fact for an operator: you change what a run does by changing state in the database, not files — which is exactly why this is a dlake skill. You reach that state with the crmpro_* tools in §2, and directly with the CLI’s data plane for the rest.

Flag table name, worth having up front. The flag table is CommercientFlags — the plural form. The singular Commercient_Flag is never a table name. Use CommercientFlags, and confirm against the tenant before acting.


1. When you touch it

  • A destination object is not syncing, or is syncing the wrong rows → a CRM_Configuration row.
  • A field is missing or landing in the wrong destination field → CRM_FieldList, or a mapping column.
  • Sync stopped entirely, or a test run “turned it off” → a flag in CommercientFlags.
  • “Did it run? What broke?” → the transaction/state tables in section 4, or crmpro_errors.

You do not install, schedule or upgrade the agent from here. This skill is the operating surface: what an operator reads and changes about a running CRMPro. Reach for the crmpro_* tools in §2 first; drop to the tables in §3–§4 for what those tools do not cover.

2. Operating CRMPro remotely — the crmpro_* tools

The control plane carries a dedicated crmpro_* tool set, and it is the preferred way to operate CRMPro. These tools drive the product’s own logic — the connection pair, the delete guard, the view that belongs to a process, the customer’s activity trail — instead of writing rows behind it. Everything runs from anywhere, over the tenant’s API key:

dlake admin <tool> --profile <tenant> [--arg value ...]

--profile is always explicit. There is deliberately no default profile — naming the tenant on every call is what stops a command landing on the wrong customer. DLAKE_PROFILE in the environment is the only shorthand.

Every crmpro_* tool is Admin-only. The key you call with must belong to a tenant user who holds the Admin role; a key minted from a non-admin user is refused with a 403 naming that role. Keys inherit the roles of the user who minted them, so mint operating keys from an Admin account — a 403 here is an account question, not a broken tool or a scope you can widen after the fact. The same rule governs the registration_* wizard tools.

No crmpro_* tool takes a user id: the platform resolves the customer from the key. Each returns its outcome directly, so you read the result rather than inferring it from a follow-up call.

Quick reference

Reads — always safe.

ToolWhat it doesKey args
crmpro_list_processesEvery sync process — the grid an operator works fromisActiveOnly (default false)
crmpro_get_processOne process in full: all 39 fields, including the stored connection idrecordId
crmpro_get_process_sqlThe CREATE VIEW script behind that process’s viewrecordId
crmpro_field_mappingThe mapping grid — one row per view column, with the saved CRM fieldrecordId
crmpro_sync_statusThe master sync flag. Pure read, no side effect
crmpro_sync_historyThe per-record sync rows (key, saved timestamp, upsert time, destination id)see --help
crmpro_sync_history_optionsThe object picker that scopes a history query
crmpro_errorsSync errors for the customer
crmpro_agent_infoThe sync agent’s own self-report
crmpro_sync_log_exportThe sync agent’s OWN log, read whole and filtered. Bounded — see belowpattern, regex, tail, page, pageSize
crmpro_sync_log_textOne 10,000-byte SOURCE page of the same log, in source orderpageIndex (1-based) plus the same window args
crmpro_flagsThe sync flags for this customer’s ERP/CRM pair, with current values
crmpro_user_flagsThe customer’s extra-info toggles
crmpro_connectionsConnection-manager entries and the ids every connection-aware call wants
crmpro_crm_objectsObjects the live CRM offers
crmpro_crm_object_fieldsFields of one CRM object, livesee --help
crmpro_tables_viewsSource tables (false) or source views (true) to build a process fromisNeedtoCreateFromView
crmpro_table_columnsColumns of one table or viewtableName
crmpro_templatesThe default-template catalogsee --help
crmpro_selected_templatesWhich templates are currently enabled (where present)

Mutations — these change what the next run does.

ToolWhat it doesKey args
crmpro_set_sync_enabledTurn the customer’s master sync on or off. Idempotent: it reads the current state, toggles only if the state differs, and reports before and afterenabled (bool)
crmpro_update_process_fieldFlip one field on one process — the everyday Is_Active style leverrecordId, fieldName, checked
crmpro_update_processChange a process. Send recordId plus only the fields you are changing; the merge happens server-side and omitted fields are preservedrecordId + the fields to change
crmpro_create_processCreate a process (and, for a table-sourced one, its view)see --help
crmpro_delete_processDelete a process and attempt to drop its viewrecordId, confirm (must be true)
crmpro_apply_templateImport default templatessee --help

Worked examples

# What is this customer syncing, and is sync even on?
dlake admin crmpro_list_processes --profile <tenant>
dlake admin crmpro_list_processes --profile <tenant> --isActiveOnly true
dlake admin crmpro_sync_status    --profile <tenant>

# One process in full — this is where the stored connection id lives
dlake admin crmpro_get_process --profile <tenant> --recordId 123

# Stop one object syncing, without touching anything else
dlake admin crmpro_update_process_field --profile <tenant> \
    --recordId 123 --fieldName Is_Active --checked false

# Master switch. Safe to run when it is already in the wanted state
dlake admin crmpro_set_sync_enabled --profile <tenant> --enabled true

# Remove a process. Refused without the confirmation
dlake admin crmpro_delete_process --profile <tenant> --recordId 123 --confirm true

For crmpro_update_process and crmpro_create_process, take the exact field names from dlake admin crmpro_update_process --help (and from a crmpro_get_process read of the row you are about to change) rather than from memory.

Reading the sync log without drowning in it

crmpro_errors reads a shared error database and crmpro_agent_info is the agent’s own self-report. Neither is the log the sync wrote. That log is, and it is often the only place a failure is explained in the engine’s own words.

It is also unbounded — a long-running customer’s log runs to hundreds of thousands of lines — so both log tools return a WINDOW, never the whole file:

  • a bare call returns the last 100 matching lines, which is where a failure is;
  • the hard ceiling is 200 lines or 64 KB, whichever comes first, and no argument raises it;
  • every result carries totalLines, matchedLines, returnedLines and truncated. Read truncated before you conclude anything. truncated: true means you are looking at part of the answer; the fix is a narrower pattern, not a bigger tail.

Ask the log a question rather than scrolling it:

# The tail, bounded
dlake crmpro log --profile <tenant>

# A question. --grep is a case-insensitive substring; --regex makes it an expression
dlake crmpro log --profile <tenant> --grep "ERROR"
dlake crmpro log --profile <tenant> --grep "^Call TR_" --regex --tail 20

# Walk the matches forwards rather than from the end
dlake crmpro log --profile <tenant> --grep ERROR --page 2 --page-size 50

# One source page, in the order the agent wrote it (byte paging, so it can cut mid-line)
dlake crmpro log --profile <tenant> --source-page 3

# The WHOLE log, unfiltered, into a file — the only way to get all of it. Prints one
# line (path, size, line count) and none of the log.
dlake crmpro log --profile <tenant> --out sync.log

Never try to pull a whole log into a conversation. If you need all of it, write it to a file with --out and read the file in pieces; the tools deliberately cannot hand it over in one call.

The log’s lines are not uniformly timestamped, so there is no time filter — pattern and the window are what you narrow with.

What these tools know that a raw row write does not

  • A process targets either a connection or the customer’s registered CRM — never both. Either it points at a named connection-manager entry (its apiAuthConfigID is set, and that entry’s name decides which CRM it talks to), or that id is null and the process targets the CRM the customer registered. On the wire the authoritative signal is apiAuthConfigID, not the CRM name: reads apply a display fallback, so a row with no stored name still shows one. Get valid ids from crmpro_connections. And although crmpro_update_process preserves what you omit, still send the connection pair deliberately whenever changing the target is the point of the edit.
  • Live CRM providers are Salesforce, HubSpot and Zoho CRM, each with automatic token refresh. Dynamics CRM is recognised but answers crm_not_supported — that is the platform telling you it cannot talk to that CRM, not a misconfigured tenant. The platform spells those last two ZohoCRM and DynamicCRM, which is what a crmName argument takes; the products are Zoho CRM and Dynamics CRM.
  • The delete-records flag has a prerequisite. Turning Is_Active_Delete_Records on is refused until the process has a non-empty Delete_SQL_Query. Non-empty is not the same as valid: a source table without a primary key produces a malformed delete query that the guard cannot catch, so read the query before you enable the flag.
  • The master sync toggle is customer-wide. crmpro_set_sync_enabled affects every process in the tenant. To stop one object, use crmpro_update_process_field on that process instead.

3. Where these tables live, and how you reach them

Everything from here down is the fallback path — the direct-table surface for what the tools in §2 do not reach: the flag and parameter tables, the cursor tables, and the XML-bearing mapping columns in §6.

CRMPro’s tables are ordinary tables in the dbo schema of the customer’s gateway database — outside the tenant’s working (platform) schema, which is usually DLO but not always. The agent itself reaches them over a direct database connection; it does not use the Data API.

When a tenant is seeded, the platform sweeps a named allowlist of gateway tables into same-named, explicit-column, single-table views in the working schema. Because each view wraps one table, SQL Server treats it as updatable — you read and write through it, omitting identity columns exactly as you would against the base table.

The CRMPro objects in that allowlist:

View created forKind
CRM_Configurationsetup
CRM_FieldListsetup
CRMProRunHistorytransaction / state
CRMPRO_DASHBOARD_ERRORtransaction / state
CRMPRO_ERROR_LOGtransaction / state
CRMPRO_DeleteRecordInfotransaction / state
CRMBackupObjectListtransaction / state
CRM_Prompt_Historypresent in the lake view set; inspect before use
TimeStampRepository, TimeStampRepositoryHistorytransaction / state (shared with TxDownloaderPro)
GenericAPISyncConfiguration, GenericAPIAuthenticationTemplateLinksetup (Connection Manager siblings)

A view exists only where the tenant actually has the table. The allowlist is enumerated against the database at seed time, so a tenant missing one simply has no view for it.

Three things are deliberately NOT in that set — plan around them rather than assuming:

  • CommercientFlags — the flag table. No working-schema view is created for it.
  • CRM_Parameters — the generic-REST mapping table. Same.
  • GenericAPIAuthonticationConfiguration — the Connection Manager credential store. Its exclusion is by design: its JSON blobs are encrypted on save, and a write through a view would bypass that encrypt-on-save path. Do not add it back, and do not try to route around it. Its non-credential siblings (GenericAPISyncConfiguration, GenericAPIAuthenticationTemplateLink) are included.

Note the spelling: the credential table’s name contains the long-standing typo ...Authontication..., while the template-link table is spelled ...Authentication.... Both are load-bearing. Copy them, don’t retype them.

Confirm what a given tenant has before planning anything:

dlake login --domain <tenant>
dlake tool get_active_schema          # the working schema these views live in
dlake admin list_views                # which CRMPro views this tenant actually got

list_views takes no arguments and is read-only; it reports schemaName, viewName, creation date and an isSystem flag per view.

Getting one onto the Data API

Reads through raw SQL work the moment the tenant is seeded — the views are ordinary objects in the active schema. Record-level CRUD is different: read_records / create_record / update_record / delete_record only see entities the Data API publishes, so each view needs the normal two-step exposure and then one restart:

dlake admin list_exposed_entities                                        # what is served today
dlake admin set_entity_exposure --entity CRM_Configuration --expose true # repeat per object
dlake admin set_entity_exposure --entity CRM_FieldList     --expose true
dlake admin restart_dab --confirm true                                   # ONE restart, at the end
dlake admin list_exposed_entities                                        # check `served: true`
set_entity_exposure argumentTypeNotes
entitystringRequired. Unqualified name in the active schema — CRM_Configuration, never dbo.CRM_Configuration
exposebooleanRequired. true adds, false removes
keyFieldsstring arrayThe addressable key column(s). Required when a key cannot be derived
confirmbooleanRequired (true) only when removing

restart_dab takes confirm (boolean, must be true) and nothing else. It briefly interrupts the tenant’s live Data API — batch every exposure change first, restart once, and do it when the customer is ready rather than as a reflex. Trust the served field over mere presence: a listed but served:false entity answers EntityNotFound, which is a missing restart, not a broken entity.

4. The tables

3a. Setup — what an operator changes

CRM_Configuration is the heart of it: one row per destination object per run, and the row is what drives that object’s behaviour. Columns (as the agent creates them):

ColumnWhat it does
IDIdentity PK. Referenced by CRM_Parameters.CRM_Configuration_ID
Is_ActivePush/sync this object to the destination
Is_Active_Get_RecordsFetch records from the destination for this object
Is_Active_Delete_RecordsDelete records in the destination for this object
Sync_OrderInteger execution order. Rows are processed in ascending Sync_Order
CRM_Object_Display_NameHuman label used throughout the logs
SQL_QueryThe source query/view the rows come from
CRM_Object_API_NameDestination object/endpoint name. May carry {placeholder} tokens (see §6)
CRM_PK_API_NameThe destination-side key field
Prefix_OF_Field_OR_Object, Postfix_OF_Field_OR_ObjectPrefix/suffix applied to field or object names
TimeStamp_PrefixLoad-bearing. The prefix that namespaces this object’s rows in TimeStampRepository
Is_Create_EntityCreate the object/table in the destination
Is_Create_FieldsCreate columns/fields in the destination
Is_In_Commercient_CRMModule-specific filter
View_Name_For_Field_CreationSource view consulted when creating destination fields
Delete_SQL_QuerySource query for the delete pass
Get_SOQL_Query, Get_SOQL_Query_TypeQuery used by the get-records pass, and its mode
Get_Run_Count, Get_Current_Run_CountInterval + counter for the get-records pass (see below)
Developer_CommentFree text
Sync_Batch_SizeRows per destination API batch (default 200)
Sync_Operation_Type1 upsert · 2 create · 3 update · 5/11 skip (default '1')
Document_Source_PathLong-text path used by the document-sync path
IsAccountMatchingAccount-matching toggle
Is_Clear_SOQL_Cache_EverytimeClear the get-records cache each run
Delete_Records_Limit, IsOverwriteDeleteLimitCap on deletes per run, and the override
File_Search_Pattern, File_Name_SeparatorFile-matching settings for the document path
Last_Run_Date, File_History_DateTimestamps maintained by the agent
IsViewNeedsToCreate, CreateViewQueryWhether to build the source view, and its body
CRMNameLegacy per-row destination-platform name
APIAuthConfigIDNullable FK into the Connection Manager. NULL means “fall back to the flags”

The last several columns are added by the agent to an existing table when they are missing, so an older tenant may genuinely lack some of them. Read the live shape before you write — never assume this list matches the tenant in front of you.

Get_Run_Count / Get_Current_Run_Count are an interval pair: when the counter reaches the interval the get-records pass runs for that row and the counter resets; otherwise the counter is incremented and the pass is skipped. So a row with a large Get_Run_Count looks “broken” for several runs by design.

CRM_FieldList is the plain relational field map — ID, Object_Name, View_Field_Name (the source column), CRM_API_Name (the destination field), DateCreated. This is the first place to look when a field is landing in the wrong destination field.

CommercientFlags — the flag table. Two columns matter: FlagName and Value, both read as strings; the agent reads the whole table at startup. The documented flags:

FlagPurposeDefault
CRM_NAMETarget platform identifier (fallback dispatch)detected from DB
FirstTimeSyncCreate database objects on this run, then exit without syncingTrue
IS_CRMPRO_SYNC_ENABLEDMaster enable/disable1
IS_SYNC_RUNNINGConcurrency lock0
CRM_PRO_CHECK_NORMAL_SYNC_LAST_RUNEnable timestamp-based change detection0
CRM_PRO_FORCE_SYNC_RUNForce a sync, ignoring change detection0
IS_TOP_10_RECORDS_SYNC_REQUIREDLimit the run to top records, for testing0
SF_PARTNER_API_VERSIONDestination API version54.0
CRM_SQL_CONNECTION_TIMEOUTSQL command timeout overridenot set
Drop_SP_From_DBRecreate stored proceduresFalse

Many more flags exist per destination platform (credentials, batch sizes, per-object hour offsets, retention intervals). Enumerate the live table rather than working from this list — it is the documented subset, not the whole surface. Flags are tenant-global: one flag row changes behaviour for every config row in that database.

CRM_Parameters drives the generic REST path — see §6. Columns: ID, CRM_Configuration_ID (→ CRM_Configuration.ID), GroupName, Key, Value, IsDefaultForAllRequest, IsActive.

Connection Manager (newer tenants). GenericAPIAuthonticationConfiguration holds one row per named connection: APIAuthConfigID (identity PK), APIAuthConfigName, APIAuthConfigurationJSON, APIAuthConfigurationJSON_Backup, APIAuthResponseJSON, IsDeleted, LastModifiedDate. GenericAPIAuthenticationTemplateLink links a connection to an auth template. A CRM_Configuration row points at a connection through APIAuthConfigID; when that is NULL — or points at a row that no longer exists — the agent falls back to CRM_NAME + the flag table.

3b. Transaction / state — what an operator inspects

ObjectColumnsRead it when
TimeStampRepositoryKey (PK, ≤900 chars), SavedTimeStamp (8-byte binary), UpSertTime, SFDCIDYou need the per-record cursor. Key is TimeStamp_Prefix + the record’s source key; SFDCID is the destination-side id
TimeStampRepositoryHistoryID, then the same four columnsYou need the history behind a cursor
CRMProRunHistoryID, RunInfo (long text), RunDateTime“Did it run, and when?”
CRMPRO_DASHBOARD_ERRORid, SyncDate, RecordType, ObjectAPIName, TimeStampPrefix, ErrorKey, ErrorDescription, NoOfRecordsCountDashboard errors for the latest run — this table is truncated each run
CRMPRO_ERROR_LOGid, SyncDate, RecordType, ObjectAPIName, TimeStampPrefix, ErrorKey, ErrorDescription, RunIDPersistent errors, grouped by RunID
CRMPRO_DeleteRecordInfoID, SObjectName, ViewName, DeletedDate, DeleteRecordXML, DeleteStatus, SFDCID, ExternalKeyAuditing what the delete pass removed
CRMBackupObjectListID, CRMObjectName, Source, EntryDate, IsCompleted, ViewName, TableName, IsDeleted, IsNeverDeleted, DeletedTableDateTracking the backup/field-tracking objects

The agent also writes local log files on the customer’s sync server (a rotating run log, an error log, a run summary, and a binary last-run-timestamp file). Those are not in the database and not reachable from dlake — ask for them if the database tables do not explain a failure.

5. Direct table CRUD — the fallback path

Use this when a crmpro_* tool does not cover what you need: the flag and parameter tables, the cursor tables, CRM_FieldList rows you want to edit relationally, and anything you are reading purely to diagnose. For listing, reading, creating, changing and deleting a process, for the master sync flag, and for the mapping grid, prefer §2 — those tools carry guards and a connection-pair discipline that a row write does not.

Argument names below are exact. Getting one wrong is worse than not acting: confirm against dlake tool <tool> --help / dlake admin <tool> --help on the tenant in front of you before a write, since the tool set evolves.

Read — always safe

# Shape first: works for VIEWS exactly as for tables
dlake tool describe_entities --entities CRM_Configuration,CRM_FieldList

# The config rows, in the order the agent will process them
dlake tool read_records --entity CRM_Configuration \
    --select ID,CRM_Object_Display_Name,Is_Active,Sync_Order,Sync_Operation_Type,TimeStamp_Prefix \
    --orderby "Sync_Order asc" --first 100

# One object's field map
dlake tool read_records --entity CRM_FieldList \
    --filter "Object_Name eq 'Account'" --first 200

# Latest errors
dlake tool read_records --entity CRMPRO_DASHBOARD_ERROR --orderby "SyncDate desc" --first 50
ToolArguments
describe_entitiesentities (string array), nameOnly (boolean). Never pass both
read_recordsentity*, select (comma-separated string), filter (OData: eq ne gt ge lt le and or not), orderby (array of "col asc" strings), first (integer page size), after (cursor)
querysql* — a single read-only SELECT (or WITH … SELECT)

* = required.

The page cap is first — not limit, top or pageSize. It is the most common mis-guess against this API.

query reaches anything in the active schema without exposure, which makes it the tool for joining a config row to its parameters or its cursor rows. Qualify with the active schema (FROM DLO.CRM_Configuration when the active schema is DLO) — unqualified names resolve against the login’s default schema and error. INFORMATION_SCHEMA.* and sys.* are refused by design; use describe_entities for shape. Results cap at 10,000 rows, and query is refused for scope-restricted keys unless the key was minted with the raw-SQL opt-in.

Write — these change the next run

# Turn one object off for the next run
dlake tool update_record --entity CRM_Configuration \
    --keys '{"ID": 12}' --fields '{"Is_Active": false}'

# Correct one field mapping
dlake tool update_record --entity CRM_FieldList \
    --keys '{"ID": 88}' --fields '{"CRM_API_Name": "Customer_Ref__c"}'

# Add a mapping row (omit the identity column)
dlake tool create_record --entity CRM_FieldList \
    --data '{"Object_Name":"Account","View_Field_Name":"CUST_NO","CRM_API_Name":"Customer_Ref__c"}'

# Remove a mapping row
dlake tool delete_record --entity CRM_FieldList --keys '{"ID": 88}'
ToolArguments
create_recordentity*, data* (object: field → value)
update_recordentity*, keys* (object: key column → value), fields* (object: field → new value)
delete_recordentity*, keys* (object: all key columns)

On Windows especially, pass object/array arguments as @file.json — inline JSON gets mangled by the shell, and @file.json is the reliable path on every platform.

Omit identity columns from a create_record bodyID on CRM_Configuration, CRM_FieldList, CRMBackupObjectList; id on the error tables. The views are updatable precisely because they wrap a single table, and they behave like the base table in this respect.

What is safe, and what is not

OperationVerdict
Any read of any of these objectsSafe.
Editing Developer_CommentSafe. Free text, no behaviour
Editing CRM_FieldList rowsChanges the next run — the intended, ordinary field-mapping edit
Toggling Is_Active / Is_Active_Get_Records / Is_Active_Delete_RecordsChanges the next run. The everyday lever — but do it with crmpro_update_process_field, which carries the delete-query guard. Also see the flag caveats in §8: a few destination modules do not honour these
Changing Sync_Order, Sync_Batch_Size, Delete_Records_LimitChanges the next run. Reversible, low blast radius
Changing Sync_Operation_TypeChanges the next run, and changes semantics — upsert vs create vs update vs skip
Changing TimeStamp_PrefixDangerous. It namespaces this object’s rows in TimeStampRepository. Change it and the object’s entire cursor history is orphaned; the next run behaves like a first run for every record
Editing TimeStampRepository rowsDangerous. This is the change-detection cursor. Deleting a row re-sends that record; editing SavedTimeStamp by hand silently mis-scopes what the next run picks up
Changing a flag rowTenant-global. One row changes behaviour for every config row in the database. See §8
Deleting a CRM_Configuration rowDestructive, and it leaves the process’s view behind. Prefer Is_Active = false, which is reversible and leaves the cursor history intact; when the process really must go, use crmpro_delete_process --confirm true, which also attempts the view drop
Anything in GenericAPIAuthonticationConfigurationOut of bounds from here. Not view-wrapped, deliberately. Use the Admin Portal’s Connection Manager

6. Field mapping — the JSON- and XML-bearing columns

Three mapping surfaces, in ascending order of how careful you need to be.

Relational: CRM_FieldList

The plainest one, and the one you will use most: View_Field_NameCRM_API_Name, per Object_Name. Read the mapping a process actually resolves with dlake admin crmpro_field_mapping --profile <tenant> --recordId <id> — one row per view column, with the saved CRM field alongside it, which is the view an operator wants before changing anything. Where you then need row-level edits the tools do not offer, these are ordinary rows with ordinary CRUD, per §5.

Placeholder tokens + CRM_Parameters (generic REST destinations)

For destinations driven by the generic REST path, request shape comes from CRM_Parameters rows keyed to a CRM_Configuration_ID, grouped by GroupName. The group names the agent uses:

APIRequest · APIHeaders · APIParameters · APISuccess · APIFailure · Paging · SkipFieldsfromBody · SkipFieldsforPrePostFix · Oauth

Within a group, Key/Value carry the settings — for example an APIRequest key of Body Type whose value mentions array makes the request body a JSON array, and APISuccess keys such as Success JArray Object Name, Success Key, Success RecordID, Success Name and Success Value tell the agent where to find the created record’s id in the response. SkipFieldsfromBody holds a comma-separated list of columns to strip from the body.

Where the field mapping happens: a Value (and CRM_Object_API_Name itself) may contain {ColumnName} tokens, which the agent substitutes from the source view’s columns at run time, per row. A token prefixed Multiple_{Multiple_ColumnName} — is substituted with that column’s values across the whole batch, comma-joined. If a token names a column the view does not have, the agent logs that the view does not contain it and skips the object for that run. So a mapping break here shows up as a silently skipped object, not an error from the destination.

CRM_Parameters is not in the seeded view allowlist (§3). Check list_views before planning to edit it through the Data API.

XML-bearing columns

Two genuinely different things wear the name “XML” here. Do not conflate them.

1. Source-view columns whose names end in QBCUSTOMFIELDSXML, or are named ADDITIONALCONTACTREFXML. These carry a real XML document in the data, and the agent expands it into destination custom fields — this is what makes name/value extension data mappable at all. Three shapes appear, each a flat list of name/value pairs. Synthetic examples:

<ArrayOfDataExtRet>
  <DataExtRet>
    <OwnerID>0</OwnerID>
    <DataExtName>Sample Field</DataExtName>
    <DataExtType>STR255TYPE</DataExtType>
    <DataExtValue>sample value</DataExtValue>
  </DataExtRet>
</ArrayOfDataExtRet>
<ArrayOfAdditionalContactRef>
  <AdditionalContactRef>
    <ContactName>Sample Contact</ContactName>
    <ContactValue>sample value</ContactValue>
  </AdditionalContactRef>
</ArrayOfAdditionalContactRef>
<ArrayOfEntityPropertyOfString>
  <EntityPropertyOfString>
    <Name>Sample Property</Name>
    <Value>sample value</Value>
  </EntityPropertyOfString>
</ArrayOfEntityPropertyOfString>

The DataExtName / ContactName / Name element is the mapping key — it is what becomes the destination field. The expansion is conditional, not universal: it is gated on the run’s source ERP and on a per-call switch, and not every destination module performs it. Establish that a given tenant’s run actually does it before promising a customer a mapping through this route. The XML lives in the customer’s source view, not in a CRMPro setup table, so you change it by changing the view or the data behind it — not by editing a config row.

2. CRMPRO_DeleteRecordInfo.DeleteRecordXML. Declared as an xml column, but the current delete-pass writers store the literal marker "1" in it. Do not expect a document there, and do not build tooling that parses it. It is a flag wearing an XML column’s clothes.

JSON-bearing columns (Connection Manager) — read-only in practice

GenericAPIAuthonticationConfiguration carries three long-text JSON columns:

ColumnHolds
APIAuthConfigurationJSONThe connection’s auth configuration. Written encrypted by the portal
APIAuthConfigurationJSON_BackupThe previous value, kept before an update
APIAuthResponseJSONThe cached auth response — tokens, refreshed by the agent after an OAuth refresh

The configuration JSON’s top level carries authonticationType (note the typo — 0 NoAuth, 1 BasicAuth, 2 OAuth1.0, 3 OAuth2.0, 4 APIKey, 5 NTLM) alongside the typed sub-objects oAuth2_0Request, oAuth2_0Response, basicAuthRequest, apiKeyRequest. Synthetic shape:

{
  "authonticationType": 3,
  "oAuth2_0Request": {
    "OAuth2_0AccessTokenURL": "https://example.invalid/oauth/token",
    "OAuth2_0HttpRequestMethod": "POST",
    "OAuth2_0HttpContentType": "application/x-www-form-urlencoded",
    "OAuth2_0TokenExpireMin": 30,
    "OAuth2_0HttpHeaderList": [{ "HeaderKey": "Accept", "HeaderValue": "application/json" }],
    "OAuth2_0HttpParamsList": [{ "ParamsKey": "client_id", "ParamsValue": "<redacted>" }],
    "FormUrlEncodedContentList": [
      { "FormUrlEncodedContentKey": "grant_type", "FormUrlEncodedContentValue": "refresh_token" }
    ]
  },
  "oAuth2_0Response": {
    "access_token": "<redacted>",
    "refresh_token": "<redacted>",
    "expires_in": 1800,
    "token_type": "bearer",
    "TokenLastCreateDate": "2026-01-01T00:00:00Z"
  },
  "basicAuthRequest": { "UserName": "<redacted>", "UserPassword": "<redacted>" },
  "apiKeyRequest": { "ApiKeyPrefix": "Bearer", "ApiKeyName": "<redacted>" }
}

APIAuthResponseJSON is the response object on its own — the same access_token / refresh_token / expires_in / token_type / TokenLastCreateDate keys, and the agent prefers it over the copy embedded in the configuration because it is the fresher one. Both readers tolerate either camelCase or snake_case token keys.

You do not edit these through the Data Lake. The credential table is excluded from the view sweep on purpose (§3), and the JSON is credential-bearing. Change connections in the Admin Portal’s Connection Manager, which owns the encrypt-on-save path. What you may usefully do from here is observe the link: read CRM_Configuration.APIAuthConfigID to see which connection a config row resolves through, and whether it is NULL (flag fallback) or set.

7. Verifying a change, and what a run does next

Verify the write itself. Read the record back:

dlake admin crmpro_get_process --profile <tenant> --recordId 12          # after a crmpro_* write
dlake tool  read_records --entity CRM_Configuration --filter "ID eq 12" --first 1   # after a row write

Read-back matters most after crmpro_update_process_field: an unrecognised fieldName reports success and changes nothing, and the record is the only place that shows it.

Nothing takes effect until the next run. CRMPro is a per-customer agent that reads its configuration and flags at startup. An edit made mid-run does not affect the run in flight, and an edit made between runs takes effect at the next one. There is no “apply now” from the dlake side — do not tell a customer a change is live because the row updated.

A run does not have to be the scheduled one. The customer’s server also runs the Commercient Receiver — a WebSocket client that stays connected to the Commercient service and starts a product’s sync when a run command arrives — so a sync can be triggered on demand rather than waiting for the product’s scheduled task. The command is sent from the Commercient service side, not from this CLI. It reaches that server only while the Receiver task exists there: the Sync Agent CLI’s status reports it as Commercient Receiver: Created or Not Created, and Not Created means on-demand runs cannot reach that machine and only the scheduled task will run it. See the dlake-syncagent skill.

What a run does afterwards: it loads the flags and the configuration rows, creates destination schema where the create flags allow it, detects changes by timestamp, transforms, calls the destination API, and writes errors and a summary. Configuration rows are processed sequentially in Sync_Order, each row resolving its own connection.

Where to look after the run:

Start with crmpro_errors and crmpro_sync_history — the fastest read of what broke and what moved. Then, for the detail those do not carry:

  1. CRMProRunHistory — a row with RunDateTime proves the agent ran.
  2. CRMPRO_DASHBOARD_ERROR — errors from the latest run only (it is truncated each run).
  3. CRMPRO_ERROR_LOG — persistent errors; filter by RunID to isolate one run.
  4. TimeStampRepositoryUpSertTime on rows whose Key starts with the object’s TimeStamp_Prefix shows the cursor actually moved.
  5. CRMPRO_DeleteRecordInfo — what the delete pass removed.

If none of those moved, the question is whether the agent ran at all — a scheduling/host question, answered from the customer’s sync server, not from here.

When a run records itself but no process moved, check §7b and §7d before the host. A run that completes having left every process untouched points at process naming, the view shape, or a NULL the engine could not read — not at scheduling. And read dlake crmpro errors --profile <tenant> in either case: it reads the shared error surface, so it works whether or not the tenant has a CRMPRO_ERROR_LOG table of its own.

7b. The source-view contract — what a process’s view must expose

SQL_Query is always SELECT * FROM DLO.<view>, so every requirement below is satisfied inside the view body. Build a view to this shape and a process syncs; depart from it and the run completes without moving records, so check these first when a sync reports success and nothing arrives.

ColumnPurpose
RecordKeyThe record’s identity. Kept out of the posted properties, used to build the TimeStampRepository key, and sent as the CRM’s external id. HubSpot processes use RecordKey; ExternalKey belongs to Salesforce processes.
SFDCIDThe destination record’s id, so an existing record is updated rather than created again. Empty string for a record that has not synced yet.
TimeStampThe clone table’s rowversion, compared with TimeStampRepository.SavedTimeStamp to find changed rows. A view without it returns no rows. crmpro_table_columns does not list this column — read the clone table or crmpro_get_process_sql to confirm it.

Use two joins against TimeStampRepository, one for identity and one for change detection:

SELECT v.*, ISNULL(idj.SFDCID, '') AS SFDCID
FROM ( /* your ERP select, aliasing the key column AS RecordKey */ ) v
-- identity: keyed on [Key] alone, so a changed row keeps its CRM id
LEFT JOIN dbo.TimeStampRepository idj
       ON idj.[Key] = 'vw_HUBSPOT_NEW_CUSTOMER::' + CAST(v.RecordKey AS varchar(200))
-- change detection: the timestamp comparison belongs here
LEFT JOIN dbo.TimeStampRepository chg
       ON chg.[Key] = 'vw_HUBSPOT_NEW_CUSTOMER::' + CAST(v.RecordKey AS varchar(200))
      AND chg.SavedTimeStamp = v.[TimeStamp]
WHERE chg.[Key] IS NULL          -- only rows still needing a push

Keep them separate. A single join serving both purposes returns no id for the rows that are about to sync, and those records are created again instead of updated. A first sync gives no warning of this, because nothing exists in the CRM yet to update.

A NULL SavedTimeStamp means changed, never unchanged. The engine writes SavedTimeStamp only on the update path, so a record created through a create or seed path gets a repository row with a NULL cursor. Written as v.[TimeStamp] = ISNULL(chg.SavedTimeStamp, v.[TimeStamp]) the predicate collapses to a tautology for exactly those rows: the chg join always matches, WHERE chg.[Key] IS NULL excludes them, and they can never qualify as changed — so the update path never runs for them and their cursor never populates. The records created first are frozen for good. Write the predicate as chg.SavedTimeStamp = v.[TimeStamp] instead; after the first update pass the engine fills the cursors in and ordinary change detection takes over. Every fresh install is exposed to this, because on a fresh install every record enters through a create path.

Source views come in three kinds, differing only in the repository join and the WHERE clause: insert-only (LEFT JOIN the repository, WHERE repo.[Key] IS NULL — a record that has synced once never reappears), update-only (INNER JOIN the repository, WHERE repo.SavedTimeStamp <> v.[TimeStamp] OR repo.SavedTimeStamp IS NULL — never creates), and insert + update, the double-join above. The NULL-cursor rule holds in all three.

When a view joins N source tables, emit the MAX rowversion across all of them. A timestamp that tracks only the base table silently misses a change made in a joined one, so the upsert never fires:

(SELECT MAX(ts) FROM (VALUES (a.[TimeStamp]), (b.[TimeStamp]), (c.[TimeStamp])) x(ts)) AS [TimeStamp]

For an aggregated child, include MAX(child.[TimeStamp]) from the aggregate in that VALUES list.

Key separators are positional. The cursor key is PREFIX::value — a double colon after the prefix. Composite keys use a single colon between parts: PREFIX::value1:value2. With a single-colon prefix the already-synced check never matches, and every row is sent again on each run.

Name processes so the name contains upsert. For Sync_Operation_Type = 1 the process name selects the sync path: include upsert (add batch for the batch path). A purely descriptive name such as Companies - ArCustomer or Contacts selects no path, and the run leaves the process untouched. Put descriptive detail in Developer_Comment, which does not affect path selection.

Write CRM_Object_API_Name in lower case: company, contact, product, deal, line_item. A capitalised name is not recognised, and the external-key property is written as externalkey instead of the mapped field.

Values must satisfy the destination CRM as well as SQL. A currency column needs an ISO code, so map an ERP that stores $ with a CASE rather than a blanket replace — real ISO codes usually sit in the same column. Email columns are validated by the CRM, so malformed source addresses are refused per record. Both appear in dlake crmpro errors per record with the CRM’s own message.

The ERP SQL login needs db_owner on the source database. Normal Sync creates and alters clone tables, table types and stored procedures there, and enables change tracking. db_datareader is enough for the connection test to pass and not enough to sync.

7c. Further rules for HubSpot processes

Keep SFDCID to Sync_Operation_Type = 1. The upsert path removes SFDCID from the posted properties; the create paths do not, and HubSpot rejects the record with Invalid names: [SFDCID]. A view built to §7b therefore belongs to an upsert process. Use Sync_Operation_Type = 1 for the work and let the upsert path handle both new and existing records.

Point association joins at the prefix that holds the parent’s id. An association resolves through the parent’s SFDCID in TimeStampRepository, so the join must use the prefix the id was stored under. Where two processes cover the same object, the one that ran first holds the ids. Read the repository keys and use the prefix present there; a join on the other prefix returns no rows.

Expect PROPERTY_DOESNT_EXIST to clear over the first few runs. Missing CRM properties are created one per pass, so the count falls each run (4, then 3, then 2, then none). Compare counts across runs before treating it as a mapping problem.

Dates need the destination’s format. Native CRM date properties take ISO-8601; custom properties are more permissive. Timezone conversion can move a date by a day, so compare one synced record with its source before loading the rest.

A plan limit is reported once for all records it refused. When a portal reaches an object cap — HubSpot allows 100 products on some tiers — the refusal arrives as a single error whose description lists every affected key, comma-joined. Count the keys in that description to see how many records need a higher tier or a narrower selection.

Use the five recognised object names. company, contact, product, deal and line_item route to the HubSpot API. A custom object name returns Unable to infer object type from: <name>; custom objects are outside this path.

The Group named commercient already exists needs no action. The property-group bootstrap re-asserts itself on each run and reports this for each group. Filter it by errorKey when reading dlake crmpro errors so it does not obscure records that genuinely failed.

7d. The silent zero-record run — the engine’s signature failure

A green run that syncs nothing writes no error anywhere. The engine wraps each object’s processing in a catch-all, so a misconfigured process logs Start … END in 0.00 seconds in the run log (SalesforceLog.txt on the sync server), the summary reads Total Data Sync : 0, and the error tables stay empty. Every item below produces exactly that symptom. Check them in order.

  1. Does the source view return rows? dlake tool query "SELECT COUNT(*) FROM <schema>.<view>". Zero means the answer is in the view, not in the engine — and where the repository rows for that prefix all carry a NULL SavedTimeStamp, it is the cursor rule in §7b.
  2. A NULL in a column the engine reads. Two independent killers. A NULL text column on the configuration rowPrefix_OF_Field_OR_Object, Postfix_OF_Field_OR_Object, Developer_Comment, Document_Source_Path, File_Search_Pattern, File_Name_Separator, Delete_SQL_Query, Get_SOQL_Query and the rest — throws Object reference not set inside the engine, which catches it. And a NULL SFDCID in the view stops the object outright. Template inserts use '' everywhere; a hand-built row must too, and the view must emit ISNULL(repo.SFDCID, '').
  3. CRM_Object_API_Name fails dispatch. It is matched case-sensitively against a baked-in per-CRM dictionary, and a wrong name or wrong case is skipped in silence. Trust a working install’s value over the template catalogue — at least one shipped template carries a misspelled object name.
  4. CRM_FieldList has no rows for that Object_Name. An object with an empty field map syncs nothing, silently. Template imports populate the table; a hand-built process must populate it too, one row per pushed view column, with Object_Name equal to the CRM_Object_API_Name value.
  5. Case anywhere else: SQL_Query against the actual view name, and TimeStamp_Prefix against the literal the view’s own TimeStampRepository join uses.

Two signals worth reading while still at zero. If the run created the destination’s custom fields, the engine is processing the row — field creation ran — so the fault is in the data step rather than in dispatch. And the recurring Cannot insert duplicate key … IX_CommercientFlags log line is benign; it is not the reason nothing synced.

What is engine-generic, and what to re-verify per CRM

Generic, and safe to rely on for any destination module: the three view kinds and their WHERE shapes, a never-NULL SFDCID, the SavedTimeStamp cursor rules, the MAX rowversion across joined tables, TimeStamp_Prefix discipline, mandatory CRM_FieldList rows, '' in every nullable configuration column, and NULL CRMName/APIAuthConfigID meaning registered-CRM dispatch.

Verified on the HubSpot module only — confirm these against a working install of the target CRM before relying on them elsewhere: the CRM_Object_API_Name dispatch tokens; the <prefix>::<key> repository-key separator; the same-name view-column-to-CRM-field convention; what Is_Create_Fields does, and how it types and marks the fields it creates; association columns (associate_*, or lookup-id handling); the owner-resolution target, which for HubSpot is a user email where an id-keyed CRM will want a user id; value serialisation for booleans, dates and currency; and IsAccountMatching semantics. Per-CRM object models bite as well — Salesforce keeps pricing on PricebookEntry rather than Product2, an OpportunityLineItem needs a PricebookEntryId, stages are a record type plus a sales process rather than a pipeline, and a standard-field upsert key such as Contact.Email is not an external id.

Building a process by hand, when crmpro_apply_template answers an opaque 400

  • Create minimal, then set the rest. crmpro_create_process takes recordType, crmObjectApiName, selectedTable, customViewName, displayName and crmPkApiName. crmPkApiName is required — omitting it is one cause of the bare [registration status 400], and so, sometimes, are the optional arguments syncOperationType, batchSize and connectionId.
  • crmpro_update_process does not persist createViewQuery, isViewNeedsToCreate or a CRM rebind, and still reports success. For those, expose CRM_Configuration and CRM_FieldList with set_entity_exposure, restart_dab once, then write the rows with dlake tool update_record.
  • TR_CreateUpdateView executes CreateViewQuery as a single batch at the start of a run where IsViewNeedsToCreate is set, then clears the flag. Use CREATE VIEW when the view is new and ALTER VIEW when it already exists.
  • crmpro_set_sync_enabled can need two calls on a fresh tenant. The dlake tool is idempotent — it reads the flag and fires the toggle only when the state differs from the request — but the endpoint it wraps is a blind toggle. Where the IS_CRMPRO_SYNC_ENABLED flag row does not exist yet, the first call toggles against a missing row, seeds it at 0 and reports after: false. Re-read crmpro_sync_status and call again; two calls are normal only on a fresh tenant. Anything driving the raw endpoint directly gets true toggle semantics.
  • The CLI shorthand for crmpro_update_process_field takes --value, not --checked.

8. Things that bite

  • The crmpro_* tools are Admin-only. The calling key must belong to a tenant user holding the Admin role; anything else gets a 403 naming that role. Keys inherit the user’s roles, so the fix is a key minted from an Admin account — not a scope change, not a retry. Same for the registration_* wizard tools.
  • --profile is never optional. There is no default profile. Omit it and the command does not quietly pick a tenant for you; DLAKE_PROFILE is the only shorthand.
  • An unknown fieldName on crmpro_update_process_field reports success and changes nothing. Only whitelisted columns are writable through it. Spell the field exactly as the record does, and read the record back with crmpro_get_process when it matters.
  • crmpro_delete_process requires --confirm true. Without it the call is refused. With it the process is gone — prefer switching Is_Active off first, and reserve deletion for a process that should not exist.
  • A process targets a connection OR the registered CRM, never both. apiAuthConfigID set means the connection-manager entry decides the CRM; null means the customer’s registered CRM. Reads apply a display fallback, so the CRM name on the wire is not proof of anything — trust apiAuthConfigID, and take valid ids from crmpro_connections. crmpro_update_process preserves fields you omit, but send the connection pair deliberately when retargeting is the point of the edit.
  • Is_Active_Delete_Records needs a Delete_SQL_Query first, and the guard only checks that the query is non-empty. A source table with no primary key yields a malformed delete the guard cannot catch — read the query yourself before enabling the flag.
  • The master sync toggle is customer-wide. crmpro_set_sync_enabled is idempotent once the flag row exists, and reports the state before and after, but it moves the whole customer. One object belongs in crmpro_update_process_field. On a fresh tenant, where the flag row does not exist yet, the first call seeds it at 0 and two calls are normal — see §7d.
  • Dynamics CRM answers crm_not_supported (the platform’s own value for it is DynamicCRM). Salesforce, HubSpot and Zoho CRM are the live providers, each refreshing its own tokens. That refusal is the platform being clear, not a tenant fault.
  • TimeStamp_Prefix is the cursor namespace. TimeStampRepository.Key is built as TimeStamp_Prefix + the record’s source key. Rename the prefix and every existing cursor row for that object is orphaned in place — no error, and the next run re-sends the whole object. Treat this column as immutable once an object has synced.
  • Editing cursor rows by hand silently changes what syncs. SavedTimeStamp is an 8-byte binary change-detection value, not a date you can eyeball. Deleting a row re-sends that record; a wrong value mis-scopes the run. Read this table freely; write to it only with a reason you can state.
  • Sync_Operation_Type is semantics, not a toggle. 1 upsert · 2 create · 3 update · 5/11 skip. Setting it to 2 on an object that already exists in the destination means the run stops updating and starts creating.
  • A “test” run can leave sync switched off. After a run made with IS_TOP_10_RECORDS_SYNC_REQUIRED set, the agent sets IS_CRMPRO_SYNC_ENABLED to 0 as well as clearing the test flag. That is deliberate — but if you turn the test flag on and walk away, the customer’s sync is disabled and the flag that disabled it is not the flag you set. Check IS_CRMPRO_SYNC_ENABLED afterwards.
  • CRM_PRO_FORCE_SYNC_RUN is one-shot. The agent resets it to 0 at the end of a run. If you find it at 0, that does not mean nobody set it.
  • FirstTimeSync = True costs you a run. That run creates database objects and exits without syncing, then sets the flag to False. This two-phase startup is intended and must be preserved. Do not set it back to True to “refresh schema” unless you intend to burn a run.
  • IS_SYNC_RUNNING can strand. It is a concurrency lock, and a crash mid-sync can leave it at 1, blocking future runs until it is manually reset to 0. That reset is the supported recovery; the enforcement lives outside the tables this skill covers, so verify the agent is genuinely not running before you clear it.
  • Flags are tenant-global; config rows are per-object. If a change should affect one object, it belongs in CRM_Configuration, not in a flag.
  • Not every destination module honours the flags. A few modules act without checking Is_Active (and some without checking Is_Active_Get_Records either) — Trello, Asana and Shopify among them. If setting Is_Active = false does not stop a sync, this is the first thing to check, not a Data Lake problem. Re-verify against the tenant’s own build.
  • DeleteRecordXML is not XML. See §6.
  • The credential JSON is off-limits from here. No view is created for the Connection Manager credential table, by decision, because a write through a view would bypass encryption on save.
  • CommercientFlags and CRM_Parameters have no seeded view either. Do not plan an edit through the Data API without checking list_views first.
  • Exposure vs restart. An entity in the scope but served: false answers EntityNotFound. That is a missing restart_dab, not a broken view. And restart once, at the end — each restart briefly interrupts the tenant’s live Data API.
  • Spelling is load-bearing. GenericAPIAuthonticationConfiguration and authonticationType carry a long-standing typo; GenericAPIAuthenticationTemplateLink does not. The agent’s flag table is CommercientFlags, not the singular form. Copy names; do not correct them.

9. Where this sits

SkillCovers
dlake-integration-setupStanding an integration up: registration, verification, seeding, then the wizard — CRM choice and the ERP connector
dlake-crmpro (this)Operating the forward leg: the crmpro_* tools, and the setup and transaction tables behind them — processes, sync control, field mapping, diagnostics
dlake-crmpro-hubspotThe HubSpot values for that leg: the object-name tokens the engine dispatches on, the configuration a HubSpot process needs, the DLO view contract for it, the seed/upsert pair, CRM_FieldList, the portal limits that shape the design, and what to check when a run pushes nothing. Read it before building a HubSpot process
dlake-crmpro-salesforceThe Salesforce values for that leg: standard plus managed-package objects, the external-id key, the single-colon repository key, the lookup ladder that makes Sync_Order a dependency order, and the create/update pairs
dlake-crmpro-shopifyThe Shopify values for that leg: the upper-case object tokens, the insert-only create legs, and the two update legs that compare the ERP value against a mirrored Shopify table instead of using the cursor
dlake-crmpro-zohocrm, dlake-crmpro-dynamicscrm, dlake-crmpro-mdc, dlake-crmpro-magentoThe same job for the other destinations the catalogue ships templates for: the objects, the repository key and the view conventions those templates set

Each destination skill carries one child page per source ERP, under erps/. The destination skill holds what is true of that CRM whatever the source; erps/<erp>.md holds what the shipped templates for that one ERP set up — the groups, the views, the prefixes, the field mapping. Every destination skill opens with an ERP table listing its children and closes the question of which one applies; read the destination skill first, then the row for the tenant’s source ERP. dlake skills show dlake-crmpro-hubspot/erps/sage100 prints one directly. | dlake-txdownloaderpro | The writeback leg: exposing the TxDownloaderPro objects and scoping a key to them | | dlake | Operating a tenant generally — schema, queries, exports, keys, the REST/GraphQL contract |

Do this after the tenant exists and has been seeded: the gateway views are created at seed time, so there is nothing to read or expose before then.