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
1. Purpose and Scope
Section titled “1. Purpose and Scope”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: “NotImplementedErroron every method… no live consumers”). It’s excluded fromERPSourceFactory’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:
| Abbreviation | Role |
|---|---|
| CS | Customer Success |
| ENG | Engineering / DevOps |
| CUST | Customer (their IT or admin contact) |
2. Prerequisites — Intake Checklist
Section titled “2. Prerequisites — Intake Checklist”CS must confirm all items before scheduling the kickoff call. Do not begin provisioning until complete.
Commercial
Section titled “Commercial”- Contract signed; ARR entered in CRM
- Support tier confirmed (standard / priority)
- Go-live target date agreed
ERP Information
Section titled “ERP Information”- 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)
ERP Credentials
Section titled “ERP Credentials”Prophet21
- API URL (e.g.,
https://api.customer.com/api/) - Service account username + password
- Service account has
API Userrole 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
Email Configuration
Section titled “Email Configuration”- 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)
User Accounts
Section titled “User Accounts”- Names and email addresses of all users to invite
- Role assignments: admin (can configure settings) vs. member (order processing only)
3. Step-by-Step Procedure
Section titled “3. Step-by-Step Procedure”Phase 1: Org, Connection, and Discovery
Section titled “Phase 1: Org, Connection, and Discovery”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:
- Validate credentials against the ERP type schema
- Encrypt and store credentials in Infisical with the path
INTEGRATION_CRED_<orgId>_<slug>_<timestamp> - Create the
integrationCredentialsanderp_connectionsDB 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 theAPI Userrole - 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:
| ERP | Explorer | How to run |
|---|---|---|
| Acumatica | AcumaticaInstanceExplorer | AcumaticaSyncOrchestrator.run_instance_explorer(connection_id) via Dagster |
| Prophet21 | P21VersionDetector + P21DatabaseDetector | Auto-runs on first sync; or trigger manually via P21VersionDetector.detect() |
| NetSuite | discover_custom_fields() | Call per entity type from netsuite/client.py |
| Odoo / Dynamics | No explorer yet | Complete 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_resolvespricing 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
BranchIDon 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_connectionsSET capability_overrides = '{ "products": true, "customers": true, "cross_references": true, "shipping_addresses": true, "ship_to_links": true, "product_images": false, "product_pricing": false}'::jsonbWHERE id = '<connectionId>';ERP type defaults (use as starting point, adjust based on explorer):
| ERP | products | customers | cross_refs | ship_addrs | ship_to_links | images | pricing |
|---|---|---|---|---|---|---|---|
prophet21 | Y | Y | Y | Y | Y | Y | Y |
netsuite | N | Y | N | N | N | N | N |
dynamics | Y | Y | N | N | N | N | N |
eclipse_e4 | N | Y | Y | Y | Y | N | N |
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
EntityAvailabilityin 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_connectionsSET 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> }}'::jsonbWHERE id = '<connectionId>';Prophet21 specifics:
access_method:"odata"(standard) or"direct_sql"(on-premise DB; requiresdatabase_configsub-object)order_creation_method:"standard"|"import_file"|"transaction_api"— confirm with customer ITodata_version:"v3"or"v4"— copy fromP21VersionInfo.odata_version
Acumatica specifics:
multi_branch: copy from profile — required for correctBranchIDhandling on order submissionpricing_strategy: copyrecommended_strategyfrom 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_connectionsSET 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
Phase 2: Platform Configuration
Section titled “Phase 2: Platform Configuration”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:
| Flag | Description | New customer default |
|---|---|---|
order_review_enabled | Access to order review UI | ON |
pdf_extraction_enabled | PDF PO extraction | ON |
email_intake_enabled | Email-based order intake | ON |
erp_sync_enabled | Dagster sync pipeline | ON |
iceberg_catalog_enabled | Iceberg catalog backend | ON |
agentic_validation_enabled | Agentic order validation | Per 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>(notclerkOrganizationId) - Traits:
connection_id,connection_type: "prophet21",clerk_organization_id(sent automatically byget_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()ignoredconnectionIdentirely 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 theconnection_<connectionId>identity, not the org identity.
Phase 3: Data Sync
Section titled “Phase 3: Data Sync”Step 9 — Initial ERP Data Sync Owner: ENG ⛔ Gate
Trigger a full initial sync via the Dagster UI:
- Navigate to Jobs → erp_sync_job
- Select partition:
<connectionId> - 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_v2dataset)
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.
Phase 4: User Setup and Email Intake
Section titled “Phase 4: User Setup and Email Intake”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
adminrole to the designated customer admin contact;memberfor 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:
- Their dedicated ERP Unlocked inbound email address (provisioned per-org, visible in the webapp settings)
- Instructions to set up a forwarding rule or SMTP relay from their order inbox to this address
- 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.
Phase 5: Validation and Handoff
Section titled “Phase 5: Validation and Handoff”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
completedfor 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:
- Login — customer logs in with their invited user account
- Product catalog — browse products; confirm SKU count and names match their ERP
- PDF PO upload — upload a sample PDF purchase order; confirm line items are extracted correctly
- Email PO — send a test email PO from a trusted sender address; confirm it appears in the inbox and is triaged as an order
- Order submission (sandbox) — if a sandbox ERP environment is available, submit a test order and confirm it appears in the ERP
- 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
- Customer confirms UAT acceptance in writing (email is sufficient)
- CS updates CRM: status → Active, go-live date, ERP type,
connectionId, Flagsmith identity - CS notifies ENG to confirm production credential path is active (no sandbox overrides remaining)
- ENG confirms Dagster scheduled sync job is running on schedule (default: daily at 2 AM UTC)
4. Decision Points
Section titled “4. Decision Points”On-Premise ERP (no cloud-accessible API)
Section titled “On-Premise ERP (no cloud-accessible API)”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.
Beta ERP (NetSuite, Odoo, Dynamics)
Section titled “Beta ERP (NetSuite, Odoo, Dynamics)”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
No Sandbox Available
Section titled “No Sandbox Available”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
5. Handoff Summary
Section titled “5. Handoff Summary”| Handoff | From | To | Trigger |
|---|---|---|---|
| Intake complete | CS | ENG | Intake checklist 100% complete |
| Provisioning complete | ENG | CS | Steps 1–9 done, internal verification passed |
| UAT ready | CS | CUST | User invites sent + email intake verified |
| Go-live sign-off | CS + CUST | CS (CRM update) | Written UAT acceptance received |
| Steady-state support | CS | Support queue | Go-live + 5 business days |
6. Rollback and Offboarding
Section titled “6. Rollback and Offboarding”If Onboarding Is Abandoned Mid-Process
Section titled “If Onboarding Is Abandoned Mid-Process”- Set
erp_connections.active = false— immediately stops sync and order routing - Delete the Infisical secret (path recorded in Step 2)
- Delete the Clerk organization (removes all user access)
- Remove the Flagsmith org identity
- Cancel / disable the Dagster partition for this
connectionId - Document reason in CRM
Do not hard-delete the
erp_connectionsrow — preserve it for audit purposes.
If a Go-Live Must Be Rolled Back
Section titled “If a Go-Live Must Be Rolled Back”- Set
erp_connections.active = false— immediate effect, no data loss - Notify customer
- Data in R2 / Iceberg persists and is recoverable
- Re-activate by setting
active = trueafter the issue is resolved
Full Offboarding (Customer Churned)
Section titled “Full Offboarding (Customer Churned)”Follow the above rollback steps, then:
- Archive Iceberg data per the data retention policy
- Close support tickets
- Update CRM status to Churned with reason