Concorbit HelpAll guides →

Importing from Other Providers

Importing jobs from another provider

Moving from ServiceM8, Jobber, or BigChange to concorbit? The concorbit:import-jobs command ingests a historical export so your new tenant lands with its real job history in place: completed work shows up on customer pages, profitability data backfills into JCP, and the dispatcher doesn't stare at an empty board on day one.

Three providers are supported:

ProviderFormatWhere the export lives
ServiceM8CSVReports → Export → Jobs
JobberCSVGear icon → Reports → Jobs → Export CSV
BigChangeXMLAdmin → Reporting → XML export

The flow

The import is always a two-step process: dry-run first, execute second. The dry-run tells you exactly what will happen without writing anything, so you can fix the export (or your CRM) before committing.

1. Dry-run

php artisan concorbit:import-jobs \
  --tenant=acme \
  --provider=servicem8 \
  --file=/tmp/sm8-export.csv \
  --dry-run

Output includes a summary table:

  • Total rows: jobs parsed from the file

  • Companies created vs Companies matched: how many customers we'd add new vs reuse from the CRM

  • Jobs created vs Jobs skipped: skipped means either a missing company name or an already-imported row (see "Re-imports" below)

  • Errors: per-row with row number + message

Read this summary carefully. A dry-run that reports "Companies created: 200, Companies matched: 5" on a tenant that already has 50 customers usually means the provider's customer names don't match yours exactly (trailing whitespace, Ltd vs Limited, etc.). Fix the export or the CRM before running for real; duplicate company records are painful to clean up later.

2. Execute

Drop the --dry-run flag:

php artisan concorbit:import-jobs \
  --tenant=acme \
  --provider=servicem8 \
  --file=/tmp/sm8-export.csv

The command runs in batches of 50 jobs per transaction. A single bad row rolls back only its batch; the rest of the import proceeds and the row shows up in the per-row errors list at the end.

What gets imported

For each row in the export:

  • Company: matched case-insensitively by name within the tenant. If not found, created with is_active = true and no other detail. You'll want to edit the address, phone, and contacts afterwards.

  • Job: always created with a fresh JOB-NNNN number from your tenant's sequence (the provider's reference is stashed in notes as [import:<provider>:<external_ref>] for dedupe). Status maps from the provider's vocabulary to concorbit's lifecycle:

    concorbit statusMaps from
    completedServiceM8 "Completed", Jobber "Complete", BigChange "Complete"
    invoicedJobber "Invoiced" / "Requires Invoicing"
    in_progressServiceM8 "Work Order", Jobber "Active", BigChange "InProgress"
    scheduledServiceM8 "Quote", Jobber "Scheduled", BigChange "Scheduled"
    on_holdJobber "On Hold"
    cancelledServiceM8 "Cancelled" / "Unsuccessful", Jobber "Cancelled" / "Archived", BigChange "Cancelled"
    completed (fallback)anything else; you can filter + fix post-import
  • Job type: if the provider's category/service name exactly matches (case-insensitively) an existing job type in your concorbit tenant, the job binds to it. Otherwise falls back to a single "Imported (historical)" type that's auto-created on first import. Split that type up afterwards from Jobs → Settings if you want cleaner categorisation.

What is NOT imported

  • Line items, parts, labour time entries. The totals from the export land in the notes for reference; they don't spawn charges. Historical profitability is not reconstructed; JCP starts from jobs you complete post-migration.

  • Engineer assignments. The export gives a name ("Tom Fielding"); we don't fuzzy-match against your user list because one-off name collisions are a real risk. The engineer name goes into notes. Reassign in the dispatch board once the import settles.

  • Attachments. ServiceM8 photos, Jobber files, BigChange docs: none come across. The vendors don't expose them in their standard exports.

  • Contacts. We import the company, not the individual contacts attached to it. The export's "primary contact" is a single field, and your CRM probably already has richer contact records. Re-link contacts to imported jobs manually.

Re-imports

The same file can be re-run safely. Every job carries an [import:<provider>:<external_ref>] marker in its notes; a second import skips any row whose marker already exists. This means you can:

  • Pull a fresh export a week later with the latest few jobs, re-run the command, and only the new ones land.

  • Rerun with a wider date window without duplicating already-imported history.

A row without an external reference (provider export had no ID column) is imported every time, so keep the file windowed tight if this applies to you.

Dealing with import errors

The most common per-row errors:

  • Missing company name: the row is skipped entirely. Fix the export (every row must have a company) and re-run.

  • SQLSTATE[23505]: Unique constraint: two rows in the same file referenced the same external ref. Dedupe the CSV first.

  • Bad date strings: the importer's safe-parse falls back to null; the job lands with no scheduled_at / actual_end_at. Check the provider's date format (some ServiceM8 exports use DD/MM/YYYY without the leading zero; Laravel's Carbon accepts most variants but not all).

Web UI

For most operators the CLI isn't the first choice. The same dry-run → execute flow is available from Jobs → Import on the web:

  1. Pick the provider (ServiceM8 / Jobber / BigChange).

  2. Upload the export file (20 MB max; larger files should go through the CLI).

  3. Review the preview numbers and per-row errors. If anything looks off, start over and fix the source.

  4. Click Execute import and confirm.

The page is behind jobs.settings: migration is an operator action, not something dispatchers need to run.

Permission

Web UI: jobs.settings. CLI (concorbit:import-jobs): runs as the platform operator, no permission gate.

Support

Dumping a sanitised copy of your export and asking in the concorbit Slack is the fastest path to fixing a stuck import; we can spot-check the parser against real vendor quirks we haven't seen yet. See the staff Slack guide for how to share files safely.