Skip to content

SOP-OB-001: Customer Onboarding — Supported ERP (Fast Path)

Version: 1.2 — Step 8 fixed 2026-08-20: p21_custom_source_enabled needs its own per-connection Flagsmith identity, not the org identity used for the other flags in that step (the override was silently inert before this). Owner: CS Lead + Engineering Lead Audience: Customer Success (CS), Engineering (ENG) Target timeline: 3–5 business days from contract signature to go-live


This SOP governs end-to-end onboarding for customers whose ERP is already registered in the ERP Unlocked integration registry. No custom engineering work is required.

Covered ERPs: Prophet21 (GA), NetSuite (beta), Microsoft Dynamics (beta), Eclipse E4 / VMS EPMS (actions-only).

SAP Business One was designed (see architecture/sap-integration-design.md) but never completed — its Dagster orchestrator and DLT source were removed as dead stubs in #3038 (2026-08-16: “NotImplementedError on every method… no live consumers”). It’s excluded from ERPSourceFactory’s supported-types list and cannot be onboarded via this SOP — route a SAP B1 prospect through SOP-OB-002 instead.

Odoo has a real order write-back path (OdooService / OdooXmlRpcClient) but no Dagster sync pipeline yet, so Step 9 (Initial ERP Data Sync) fails for it the same way SAP B1 does. Credentials can still be collected for the write-back path, but don’t commit to the 3–5 day fast-path timeline for an Odoo customer — loop in ENG first.

Not covered: Any ERP not in the registry above. See SOP-OB-002.

Role key:

AbbreviationRole
CSCustomer Success
ENGEngineering / DevOps
CUSTCustomer (their IT or admin contact)

CS must confirm all items before scheduling the kickoff call. Do not begin provisioning until complete.

  • Contract signed; ARR entered in CRM
  • Support tier confirmed (standard / priority)
  • Go-live target date agreed
  • ERP type confirmed and matched to a registry slug (e.g., prophet21, netsuite)
  • ERP version / release confirmed (required for P21 version detection)
  • API access method confirmed: REST OData, Service Layer, SuiteQL, etc.
  • Network access confirmed: cloud-hosted API vs. on-premise (VPN/agent required?)
  • Sandbox / test environment available? (Required before any write-operation testing)

Prophet21

  • API URL (e.g., https://api.customer.com/api/)
  • Service account username + password
  • Service account has API User role with read access to items, customers, ship-to addresses, and order write

NetSuite

  • Account ID
  • Consumer Key + Consumer Secret
  • Token ID + Token Secret
  • Token-Based Authentication (TBA) enabled in NetSuite

Odoo

  • API URL
  • Database name
  • Username + API key

Microsoft Dynamics

  • Tenant ID
  • Client ID + Client Secret
  • Resource URL
  • Trusted sender domains (e.g., acme.com, acmecorp.com) — all domains from which their customers send POs
  • Any specific trusted sender patterns — regex, not a glob (e.g., purchasing-[^@]+@acme[.]com)
  • Customer’s order inbox address (where they will forward incoming POs)
  • Customer’s ERP system sender address, if the ERP sends automated POs (add as trusted sender)
  • Names and email addresses of all users to invite
  • Role assignments: admin (can configure settings) vs. member (order processing only)


Step 1 — Create Clerk Organization Owner: CS

Create the customer organization in the Clerk Dashboard.

  • Name: use the legal business name exactly as it appears on the contract
  • Slug: lowercase, hyphens, no spaces (e.g., acme-distribution)
  • Record the clerkOrganizationId (format: org_...) — required for all subsequent steps

Verify: organization appears in Clerk dashboard. No users added yet.


Step 2 — Create ERP Connection Record Owner: ENG

Via the webapp admin panel or API, create the ERP connection:

POST /api/integrations/connections
{
"integrationSlug": "<slug>",
"name": "<Customer Name> — <ERP Type>",
"credentials": { <per-ERP fields from intake checklist> }
}

The platform will:

  1. Validate credentials against the ERP type schema
  2. Encrypt and store credentials in Infisical with the path INTEGRATION_CRED_<orgId>_<slug>_<timestamp>
  3. Create the integrationCredentials and erp_connections DB records

Record: connectionId (UUID), Infisical secret path.

Note for NetSuite: TBA credentials are validated at save time rather than via a live test call. Confirm the TBA tokens are active in NetSuite admin before saving.

Note for Odoo / Dynamics (beta): credential validation is basic schema-only — verify connectivity manually in Step 3.


Step 3 — Test Connection Owner: ENG ⛔ Gate

POST /api/integrations/connections/test
{
"integrationSlug": "<slug>",
"credentials": { <same credentials used in Step 2> }
}

Expected response: { "success": true }.

If the test fails:

  • Verify credentials are typed correctly (copy-paste from intake form, not retyped)
  • Check network access: on-prem ERP may need a firewall rule or VPN before the API is reachable
  • Prophet21: confirm API URL ends with /api/ and the service account has the API User role
  • NetSuite: confirm TBA is enabled under Setup → Company → Enable Features → SuiteCloud
  • Escalate to CUST IT with a specific error message if credentials are confirmed correct

Do not proceed to Step 4 until this test passes.


Step 4 — Run Instance Explorer Owner: ENG 🔍

The Instance Explorer probes the customer’s ERP to discover its actual configuration, licensed modules, custom fields, and data volumes. Even for fully supported ERPs, every customer has different versions, enabled modules, and customizations — this step surfaces those differences before you configure anything.

The output is a TenantCapabilityProfile stored in erp_connections.extra_config["tenant_capability_profile"]. It directly informs Steps 5 and 6.

By ERP type:

ERPExplorerHow to run
AcumaticaAcumaticaInstanceExplorerAcumaticaSyncOrchestrator.run_instance_explorer(connection_id) via Dagster
Prophet21P21VersionDetector + P21DatabaseDetectorAuto-runs on first sync; or trigger manually via P21VersionDetector.detect()
NetSuitediscover_custom_fields()Call per entity type from netsuite/client.py
Odoo / DynamicsNo explorer yetComplete the manual checklist below

For Acumatica — automated profile includes:

  • ERP version and endpoint version
  • Entity availability (customers, products, orders, invoices, warehouses, branches)
  • Pricing configuration: native sales prices, customer discounts, volume pricing tiers, third-party engine (RPM, Cavallo)
  • Custom fields (Usr*) on Customer, SalesOrder, StockItem
  • Approximate entity record counts (drives sync time estimates)
  • Multi-branch / multi-company structure

For Prophet21 — automated profile includes:

  • OData version (v3 vs v4) — determines filter syntax used in all queries
  • Data Services endpoint availability — unlocks faster v2 sync path
  • Database access viability (if on-premise DB credentials provided)

For NetSuite — automated profile includes:

  • Custom field IDs and labels per record type (Customer, Item, SalesOrder)

For ERPs without an explorer — manual discovery checklist:

  • Which entity modules are licensed? (products, customers, cross-references, shipping addresses)
  • Are there any custom fields on customer, item, or order records?
  • What is the pricing model? (list price only, customer-specific pricing, volume tiers, third-party pricing engine)
  • Multi-company or multi-branch setup?
  • Approximate record counts for products and customers
  • Any known data quality issues or gaps the customer has flagged?

Review findings with ENG + CS. Flag anything that diverges from the ERP type’s defaults — these drive the next two steps. Common findings that require special handling:

  • Custom fields the customer expects to be populated on submitted orders
  • Third-party pricing engines (requires erp_resolves pricing strategy — do not push prices)
  • Unusually large catalogs (> 100k products → adjust batch config before full sync)
  • Non-standard API versions or disabled endpoints
  • Multi-branch setup (requires BranchID on order submission)

Document all non-default findings in the customer’s CRM record. These become the handoff notes for the support team and any future engineering work.


Step 5 — Configure Capability Overrides Owner: ENG

Set capability_overrides on the erp_connections row using the explorer findings from Step 4. Start with the ERP type defaults and adjust for what the explorer actually found.

UPDATE erp_connections
SET capability_overrides = '{
"products": true,
"customers": true,
"cross_references": true,
"shipping_addresses": true,
"ship_to_links": true,
"product_images": false,
"product_pricing": false
}'::jsonb
WHERE id = '<connectionId>';

ERP type defaults (use as starting point, adjust based on explorer):

ERPproductscustomerscross_refsship_addrsship_to_linksimagespricing
prophet21YYYYYYY
netsuiteNYNNNNN
dynamicsYYNNNNN
eclipse_e4NYYYYNN

sap_b1 and odoo aren’t listed — neither has a Dagster sync pipeline (see Section 1), so there’s nothing to set capability_overrides against yet.

Adjustments driven by explorer findings:

  • If the explorer found that an entity endpoint returned 404 or 0 records → disable that capability
  • If the explorer found cross-references are not licensed → disable cross_references
  • If the explorer found a third-party pricing engine → disable product_pricing (ERP resolves pricing)
  • For Acumatica: the EntityAvailability in the profile maps directly to these flags

Note the reason for any deviation from type defaults in CRM.


Step 6 — Configure Extra Config Owner: ENG

Set extra_config JSONB. For ERPs with an explorer, many of these values are auto-populated in the TenantCapabilityProfile — copy them into extra_config for runtime use:

UPDATE erp_connections
SET extra_config = '{
"api_version": "<from explorer or confirmed version>",
"access_method": "odata",
"order_creation_method": "standard",
"tenant_capability_profile": { <paste profile JSON from Step 4> }
}'::jsonb
WHERE id = '<connectionId>';

Prophet21 specifics:

  • access_method: "odata" (standard) or "direct_sql" (on-premise DB; requires database_config sub-object)
  • order_creation_method: "standard" | "import_file" | "transaction_api" — confirm with customer IT
  • odata_version: "v3" or "v4" — copy from P21VersionInfo.odata_version

Acumatica specifics:

  • multi_branch: copy from profile — required for correct BranchID handling on order submission
  • pricing_strategy: copy recommended_strategy from the pricing profile (erp_resolves | resolve_then_compare | override)
  • endpoint_version: copy from profile — required for correct API path construction

For all other ERPs: leave extra_config at minimal defaults unless explorer or customer IT identified specific overrides needed.


Step 7 — Configure Email Settings Owner: ENG (data from CS intake)

-- trusted_sender_domains/trusted_sender_patterns live inside the email_config
-- JSONB column, not as top-level erp_connections columns. Merge with `||` so
-- other existing email_config keys (e.g. excluded_sender_addresses) survive.
-- Patterns are matched with Python `re` / JS `RegExp`, NOT shell globs — `*`
-- repeats the preceding character, it doesn't mean "any characters". Prefer
-- trusted_sender_domains when a plain domain match is enough.
UPDATE erp_connections
SET email_config = coalesce(email_config, '{}'::jsonb) || jsonb_build_object(
'trusted_sender_domains', ARRAY['acme.com', 'acmecorp.com'],
'trusted_sender_patterns', ARRAY['purchasing-[^@]+@acme[.]com']
)
WHERE id = '<connectionId>';

Rules:

  • Include all domains from which the customer’s own customers send POs
  • Include the ERP’s outbound sender address if the ERP itself sends automated PO emails
  • Do not add catch-all patterns that would pass unrelated notification or marketing emails — the LLM triage layer filters these, but the trusted-sender gate is the first line of defense and a misconfiguration here bypasses LLM triage entirely


Step 8 — Provision Flagsmith Feature Flags Owner: ENG

In Flagsmith, create an identity using the clerkOrganizationId as the identifier. Apply overrides only for flags that differ from environment defaults:

FlagDescriptionNew customer default
order_review_enabledAccess to order review UION
pdf_extraction_enabledPDF PO extractionON
email_intake_enabledEmail-based order intakeON
erp_sync_enabledDagster sync pipelineON
iceberg_catalog_enabledIceberg catalog backendON
agentic_validation_enabledAgentic order validationPer sales agreement

Only set overrides that differ from the environment default. Avoid over-specifying — flags without overrides inherit the environment default, which is what you want for standard customers.

p21_custom_source_enabled (P21 only) is a different identity — do not set it on the org identity above. It gates which Prophet21 sync orchestrator (V1 vs. V2) ERPSourceFactory builds, which is decided per connection, not per org — a P21 customer can have more than one connection (e.g. a sandbox alongside production), and each needs its own value. Create a separate Flagsmith identity per P21 connectionId:

  • Identifier: connection_<connectionId> (not clerkOrganizationId)
  • Traits: connection_id, connection_type: "prophet21", clerk_organization_id (sent automatically by get_p21_custom_source_enabled() every time it evaluates the flag for this connection — you only need to add the override, not pre-create the identity)
  • New customer default: environment default (leave unset) unless this specific P21 connection needs to diverge from it

Before engineering fixed this (2026-08), get_p21_custom_source_enabled() ignored connectionId entirely and only ever read the environment-wide default — so a per-customer override set on the org identity (as this step used to instruct) silently did nothing. If a P21 customer’s sync ever looks like it’s ignoring an override you set here, confirm the override is on the connection_<connectionId> identity, not the org identity.



Step 9 — Initial ERP Data Sync Owner: ENG ⛔ Gate

Trigger a full initial sync via the Dagster UI:

  1. Navigate to Jobs → erp_sync_job
  2. Select partition: <connectionId>
  3. Launch run with config: { "full_sync": true }

The sync pipeline will:

  • Call ERPSourceFactory.create_orchestrator(connection_id)
  • Run all enabled entity types based on capability overrides (Step 5)
  • Write records to R2 Iceberg tables (erp_data_native_v2 dataset)

Estimated sync times (products as the largest entity):

  • < 10k products: 5–15 min
  • 10k–100k products: 30–90 min
  • 100k products: coordinate with ENG for batch rate-limit config

If sync fails: check Dagster run logs for the full traceback. Common causes: ERP API rate limit hit (reduce batch size in extra_config), credentials expired (re-test connection).

Do not invite users until at least one completed sync run exists for both products and customers.



Step 10 — Send User Invites Owner: CS

In the Clerk dashboard, navigate to the organization created in Step 1:

  • Send invitation to each email address from the intake checklist
  • Assign admin role to the designated customer admin contact; member for all others
  • Invites expire after 7 days — note the expiry date and resend if not accepted

Step 11 — Configure Email Intake (Forward Rules) Owner: CS + CUST

Provide the customer with:

  1. Their dedicated ERP Unlocked inbound email address (provisioned per-org, visible in the webapp settings)
  2. Instructions to set up a forwarding rule or SMTP relay from their order inbox to this address
  3. A note that only senders matching the trusted domains/patterns configured in Step 7 will be processed without LLM triage review

CUST action required: configure the forward rule in their mail system.

CS verification: send a test email from a trusted sender address and confirm it appears in the webapp inbox before scheduling UAT.



Step 12 — Internal Verification Owner: ENG

Run through this checklist before scheduling the customer UAT call:

  • Connection test passes (/api/integrations/connections/test)
  • Instance Explorer completed; findings documented in CRM; any customizations noted
  • Capability overrides set correctly per explorer findings (no enabled capability with 0 records)
  • At least one full sync completed with status completed for products and customers
  • Product search returns expected results in the webapp (spot-check 3–5 known SKUs)
  • Customer lookup returns expected results (spot-check 2–3 known accounts)
  • Flagsmith identity verified active for this org in production environment
  • Test email forwarded and triaged correctly (check webapp / Temporal logs)
  • No active alerts or errors on the Dagster scheduled sync job for this connection

Step 13 — Customer UAT Owner: CS (CUST performs tests)

Schedule a 1-hour call. Walk the customer through each test:

  1. Login — customer logs in with their invited user account
  2. Product catalog — browse products; confirm SKU count and names match their ERP
  3. PDF PO upload — upload a sample PDF purchase order; confirm line items are extracted correctly
  4. Email PO — send a test email PO from a trusted sender address; confirm it appears in the inbox and is triaged as an order
  5. Order submission (sandbox) — if a sandbox ERP environment is available, submit a test order and confirm it appears in the ERP
  6. Order review — review the order status and any agentic validation results

Acceptance criteria — all must pass before go-live:

  • Customer can log in successfully
  • Product catalog is populated and searchable
  • At least one PDF order extracted with correct line items
  • At least one emailed PO received and triaged correctly
  • Test order submitted to ERP sandbox (or this step explicitly deferred with written agreement)
  • No P0 or P1 bugs blocking core usage

Step 14 — Go-Live Sign-Off Owner: CS + CUST

  1. Customer confirms UAT acceptance in writing (email is sufficient)
  2. CS updates CRM: status → Active, go-live date, ERP type, connectionId, Flagsmith identity
  3. CS notifies ENG to confirm production credential path is active (no sandbox overrides remaining)
  4. ENG confirms Dagster scheduled sync job is running on schedule (default: daily at 2 AM UTC)

If the customer’s ERP is on-premise with no publicly routable API endpoint, pause at Step 3.

Options:

  • VPN / site-to-site tunnel: coordinate with CUST IT and ENG to establish a tunnel to the Dagster environment. Adds 5–10 business days.
  • Dagster hybrid agent deployed on-premise: the agent runs in the customer’s network and connects outbound. See docs/deployment/DAGSTER-PLUS-HYBRID-MIGRATION.md.

Document the chosen approach in CRM before proceeding.

Before committing to a go-live date, confirm that the beta integration covers the customer’s required use case:

  • Order write-back may not be supported for all beta ERPs
  • Document any limitations in the contract addendum
  • Set customer expectations that beta integrations may receive breaking changes with notice

If the customer has no sandbox ERP environment, order write operations cannot be tested before go-live.

  • Perform Steps 1–12 as normal (read sync is safe)
  • At Step 13: complete all UAT items except order submission; note the skip in writing
  • Communicate this risk to the customer and get written acknowledgment before go-live
  • Plan a post-go-live sandbox test within 30 days

HandoffFromToTrigger
Intake completeCSENGIntake checklist 100% complete
Provisioning completeENGCSSteps 1–9 done, internal verification passed
UAT readyCSCUSTUser invites sent + email intake verified
Go-live sign-offCS + CUSTCS (CRM update)Written UAT acceptance received
Steady-state supportCSSupport queueGo-live + 5 business days

  1. Set erp_connections.active = false — immediately stops sync and order routing
  2. Delete the Infisical secret (path recorded in Step 2)
  3. Delete the Clerk organization (removes all user access)
  4. Remove the Flagsmith org identity
  5. Cancel / disable the Dagster partition for this connectionId
  6. Document reason in CRM

Do not hard-delete the erp_connections row — preserve it for audit purposes.

  1. Set erp_connections.active = false — immediate effect, no data loss
  2. Notify customer
  3. Data in R2 / Iceberg persists and is recoverable
  4. Re-activate by setting active = true after the issue is resolved

Follow the above rollback steps, then:

  • Archive Iceberg data per the data retention policy
  • Close support tickets
  • Update CRM status to Churned with reason