We're live in beta — earn up to 12,000 credits by signing up today. Get started
Operations

Manage jobs in ServiceM8

Read this before creating, updating, scheduling, costing or completing a job in ServiceM8, or before adding anything to a job (contacts, badges, materials, labour, notes, payments).

  • 0 installs
  • v1
  • Updated Sep 10, 2026
Written for ServiceM8

Written and maintained by FloConnector. Install it as kept updated and your copy follows our revisions; install it as your own and it never changes unless you change it.

SKILL.md 5.3 KB

Manage jobs in ServiceM8

A job is the spine of ServiceM8. Almost everything else (contacts, materials, labour, forms, photos, invoices) hangs off one, and almost nothing can be created without one existing first. Get the job right and the rest is attachment.

There is no single create_job call that does the whole thing. A real job is assembled from four to eight calls, and the order matters because each one needs the UUID the last one returned.

Before you write anything

A job needs a client. company_uuid is the client (the ServiceM8 API says company, the UI says client, they are the same record). Search before you create one, because duplicate clients are the most common mess in a ServiceM8 account and they are painful to merge later.

servicem8_list_clients   filter: "name eq 'Acme Property Group'"

If nothing comes back, widen it before giving up. Client names drift (Acme, Acme Property, ACME Property Grp). servicem8_search across record types is better than three guesses at an exact match. Only create a client once you are confident there is no existing one.

Know which status you are creating. ServiceM8 has exactly four, they are case sensitive, and they drive the Dispatch Board:

StatusMeans
QuotePriced but not won. Not scheduled work
Work OrderWon and live. This is the working state
CompletedWork finished
UnsuccessfulLost, cancelled or abandoned

Set status and let ServiceM8 stamp the matching date. Do not set quote_date, work_order_date, completion_date or unsuccessful_date to move a job’s state. They are writable only so a data migration can backdate them.

The standard sequence

  1. Resolve or create the client. servicem8_list_clients then servicem8_create_client.
  2. Create the job. servicem8_create_job with at minimum company_uuid, status, job_address and job_description. Everything else can follow.
  3. Add the site contact. servicem8_create_job_contact. The job record itself carries no phone or email fields, so without this step nobody can be called. See references/attach-to-a-job.md.
  4. Classify it. category_uuid for reporting, badges for visual flags, queue_uuid for workflow position. See references/classifying-a-job.md.
  5. Schedule it. servicem8_create_job_allocation puts it on a staff member’s day. This one has a hidden requirement, covered in the reference.
  6. Cost it as work happens. Materials for parts, activities for labour. See references/materials-and-labour.md.
  7. Close it. Set work_done_description, move status to Completed, then ready_to_invoice.

Not every job needs all seven. A quote is usually steps 1 to 4. Read references/create-a-job.md for the full worked example with payloads.

What lives where

Ownership in ServiceM8 is not always where you would guess. Getting this wrong produces writes that succeed and change nothing.

You want to recordIt lives on
Customer phone, email, site contactjobcontact, never the job
Parts, products, ad hoc line itemsjobmaterial
Labour, time on site, timesheet hoursjobactivity
Free text history, internal commentsnote
Tasks and to dostask (which silently also creates a checklist row)
Photos and documentsattachment
Money receivedjobpayment
Safety, compliance, checklists filled in the fielda Form, see the servicem8-build-forms skill

Reading jobs back

servicem8_list_jobs takes a $filter. Use it, because an account can hold tens of thousands of jobs.

status eq 'Work Order'
company_uuid eq '<uuid>'
edit_date gt '2026-09-01 00:00:00'

For anything fuzzy, servicem8_search_jobs_semantic beats guessing filter syntax. For counting or totalling across a large set, load the jobs and aggregate rather than reading them one at a time.

Do not learn these the hard way

A short list of things that are true, verified against a live account, and not in the API documentation. The full set is in references/gotchas.md, and it is worth reading once before your first write.

  • A returned recordUuid is not proof a record exists. Several ServiceM8 write endpoints return errorCode 0, "OK" and a plausible UUID while writing nothing. Read the record back after any create you care about.
  • created_by_staff_uuid is always ignored. Every record is stamped with the connected app’s own staff identity, whatever you send.
  • job.badges writes as an array and reads back as a JSON string. Do not assert the wire type.
  • Archiving is one way. ServiceM8 soft deletes by setting active to 0, and active is not writable on any resource, so nothing deleted through the API can be restored through the API.
  • Non-ASCII gets transliterated. An em dash in job_description comes back as --.

When something fails

Report what actually happened rather than retrying blind.

  • A 403 is a scope problem, not a token problem. It will fail identically on a retry. Name the scope the call needed.
  • A 429 is rate limiting. Back off, do not hammer.
  • A write that returns OK but does not read back is the silent no op class above. Say so plainly instead of reporting success.

Reference files

Everything the skill tells your AI to read, exactly as it ships in the zip.

references/attach-to-a-job.md 2.8 KB
# Attaching things to a job

Everything below needs a `job_uuid` and is a separate record with its own lifecycle.

## Contacts

The job has no phone or email fields. This is the only place they live.

```
servicem8_create_job_contact
  job_uuid: "<uuid>"
  type: "JOB"
  first: "Dana"
  last: "Whitfield"
  mobile: "+61400111222"
  email: "dana@example.com"
```

`type` values: `JOB`, `BILLING`, `PROPERTY OWNER`, `PROPERTY MANAGER`. A job can carry one of each.

**Side effect.** This also writes a `CompanyContact` on the client with `is_primary_contact: 1` and a different UUID. `servicem8_delete_job_contact` does not remove it. Clean up with `servicem8_delete_company_contact` if you created it by mistake.

## Notes

Free text history on the job. Visible to staff, not to the customer.

```
servicem8_create_note
  related_object: "job"
  related_object_uuid: "<job uuid>"
  note: "Customer called, prefers morning. Dog on site, friendly."
```

`action_required` is documented as an "optional follow up action description" and is actually a `0`/`1` flag. Any sentence you put in it is stored as `"0"`. Put the description in `note`.

## Tasks

```
servicem8_create_task
  job_uuid: "<uuid>"
  name: "Order replacement isolation valve"
  task_details: "Reece Newstead, part 4471-B"
  due_date: "2026-09-14"
```

Two traps:

- **It also creates a JobChecklist row** named `"<task name>\n<task_details>"` with its own UUID. `servicem8_delete_task` does not remove it. Delete it separately or it orphans on the job permanently.
- **`due_date` drifts forward one day per write.** Vendor side timezone handling. A task updated repeatedly walks its due date into the future indefinitely. Set it once, and if you must update the task, re send the date you actually want.

## Checklists

```
servicem8_create_job_checklist
  job_uuid: "<uuid>"
  name: "Isolate at switchboard"
```

Useful on their own for pre start steps. Note that tasks create these behind your back, so a job's checklist may contain rows nobody added deliberately.

## Attachments and photos

`servicem8_list_attachments` with `related_object_uuid eq '<job uuid>'` gives you everything filed against a job.

Attachment names are frequently useless (`Photo`, `IMG_1234`). If you need to choose between photos, look at them with `servicem8_get_job_photos` first rather than picking on filename.

## Payments

```
servicem8_create_job_payment
  job_uuid: "<uuid>"
  amount: 1450.00
  payment_method: "Bank Transfer"
  timestamp: "2026-09-16 09:30:00"
```

`is_deposit` is readable but absent from the write schema, so a deposit cannot be marked as one through the API.

## Forms

Safety checks, compliance sheets, field reports. These are their own subsystem with their own field vocabulary and a Word template behind them. See the `servicem8-build-forms` skill rather than trying to drive them from here.
references/classifying-a-job.md 2.2 KB
# Categories, badges and queues

Three different classification systems that people routinely confuse.

## Category, one per job, for reporting

`category_uuid` on the job. This is what the account's revenue and job count reporting slices by, so it is the one that matters commercially. Typical values are service lines: `Plumbing`, `Electrical`, `Maintenance Contract`, `Warranty`.

```
servicem8_list_categories
servicem8_update_job   uuid: "<job uuid>"   category_uuid: "<uuid>"
```

Resolve the name to a UUID first. Categories are a small, stable reference set, so listing them once and reusing the map across a batch is fine.

## Badges, many per job, for visual flags

`badges` on the job, an array of badge UUIDs. Badges are the coloured markers on the Dispatch Board: `Urgent`, `Awaiting Parts`, `Site Induction Required`, `Do Not Contact Before 9am`.

```
servicem8_list_badges
servicem8_update_job   uuid: "<job uuid>"   badges: ["<uuid>", "<uuid>"]
```

**Write an array, read a string.** The read back is a JSON encoded string, `"[\"uuid\"]"`, not an array. Parse it before comparing.

**Updating is a replace, not an add.** Sending `badges` overwrites the whole set. To add one badge, read the current set, parse it, append, and send the union. Sending just the new badge silently removes every other one.

Badges also connect to Forms: a form carries a `badge_name`, and `badge_mandatory_state` decides whether the badge blocks check in or check out until the form is filled.

## Queues, one per job, for workflow position

`queue_uuid`, plus `queue_assigned_staff_uuid` for ownership inside the queue and `queue_expiry_date` for when it falls out.

Queues are the account's own workflow stages: `Awaiting Quote Approval`, `To Be Scheduled`, `Parts On Order`, `Awaiting Payment`. They are the closest thing ServiceM8 has to a pipeline.

```
servicem8_list_job_queues
servicem8_update_job
  uuid: "<job uuid>"
  queue_uuid: "<uuid>"
  queue_assigned_staff_uuid: "<staff uuid>"
```

## Which to use

| Question | Use |
|---|---|
| What kind of work is this, for reporting | Category |
| What does the team need to notice at a glance | Badge |
| What stage of our process is it at | Queue |
| Is it quoted, live, done or lost | Status, not any of these |
references/create-a-job.md 4.3 KB
# Creating a job, worked through

A complete new job for a new customer, with the calls in the order they have to happen.

## 1. Find the client, or make one

```
servicem8_list_clients   filter: "name eq 'Acme Property Group'"
```

Nothing back? Try `servicem8_search` with the phone number or a partial name before creating. Duplicate clients are hard to undo.

```
servicem8_create_client
  name: "Acme Property Group"
  address: "12 Wharf Road, Newstead QLD 4006"
  is_individual: 0
```

Returns `recordUuid`. That is your `company_uuid`.

Note `is_individual` is readable but not writable, so a residential client cannot be flagged as such through the API. It defaults to a business. Say so if the distinction matters to the customer.

Also note `billing_attention` is documented as a string but behaves as a `0`/`1` flag. Any text you put in it is stored as `"0"`. Do not use it for a name.

## 2. Create the job

```
servicem8_create_job
  company_uuid: "<client uuid>"
  status: "Work Order"
  job_address: "12 Wharf Road, Newstead QLD 4006"
  job_description: "Replace failed hot water unit, 250L electric. Access via rear gate."
  category_uuid: "<category uuid>"
  purchase_order_number: "PO-88231"
```

Returns `recordUuid`. That is your `job_uuid`, and everything below needs it.

`job_address` is geocoded by ServiceM8. It rewrites `lat`, `lng` and the `geo_*` fields from the address you give it, so do not bother setting them. They are writable only because the geocoder occasionally lands a bad address on a state centroid and an override is the only fix.

`job_description` is what the field staff read. Write it for them: what, where, access, and anything that stops the visit being wasted.

## 3. Add the contact

The job record has no phone or email fields at all. Without this step the job is uncontactable.

```
servicem8_create_job_contact
  job_uuid: "<job uuid>"
  first: "Dana"
  last: "Whitfield"
  type: "JOB"
  mobile: "+61400111222"
  email: "dana@acmeproperty.example"
```

`type` distinguishes the roles a job can carry: `JOB` (site contact), `BILLING`, `PROPERTY OWNER`, `PROPERTY MANAGER`. Add more than one where they differ.

Be aware this call also creates a `CompanyContact` row on the client, flagged `is_primary_contact: 1`, with its own separate UUID. Deleting the job contact does not remove it. If you create job contacts in bulk you will accumulate client contacts nobody asked for.

## 4. Classify

See [classifying-a-job.md](classifying-a-job.md). Briefly:

```
servicem8_update_job
  uuid: "<job uuid>"
  category_uuid: "<uuid>"
  badges: ["<badge uuid>", "<badge uuid>"]
  queue_uuid: "<uuid>"
```

## 5. Schedule

```
servicem8_create_job_allocation
  job_uuid: "<job uuid>"
  staff_uuid: "<staff uuid>"
  allocation_window_uuid: "<window uuid>"
  allocation_date: "2026-09-15"
```

`allocation_window_uuid` is **mandatory**, despite the schema marking only `job_uuid` as required. Omit it and the call is rejected. Get the account's windows from `servicem8_list_allocation_windows` first.

## 6. Record what happened

As work proceeds, materials for parts and activities for labour. Both covered in [materials-and-labour.md](materials-and-labour.md).

## 7. Close it out

```
servicem8_update_job
  uuid: "<job uuid>"
  work_done_description: "Removed and disposed of failed 250L unit. Installed new Rheem 250L electric, tested, no leaks. Isolation valve replaced."
  status: "Completed"
  ready_to_invoice: 1
```

`completion_date` stamps itself off the status change. `work_done_description` is what the customer sees on the invoice, so write it as a customer would want to read it, not as a diary entry.

## Creating from a template instead

If the account has job templates, `servicem8_create_job_from_template` is faster and carries the template's materials and checklists across.

It returns a different envelope from every other create in the connector: `{jobUUID, location, message}` rather than `recordUuid`, and it does not return a job number. Read the `jobUUID` key, not `recordUuid`.

## Verify before you report success

After a create you care about, read it back:

```
servicem8_get_job   uuid: "<the uuid you were handed>"
```

This is not paranoia. Several ServiceM8 write endpoints return a success envelope and a plausible UUID for a record that was never written. See [gotchas.md](gotchas.md).
references/finding-jobs.md 2.1 KB
# Finding jobs without pulling the whole account

A ServiceM8 account can hold tens of thousands of jobs. Never list them unfiltered and then filter in your head.

## Filters

`servicem8_list_jobs` takes a `$filter` string. Comparison operators are the OData style ones ServiceM8 supports:

```
status eq 'Work Order'
company_uuid eq '019fb116-a311-7f26-a11e-fa7211decdfb'
edit_date gt '2026-09-01 00:00:00'
date ge '2026-09-01' and date le '2026-09-30'
```

Dates are strings in `YYYY-MM-DD HH:MM:SS`. Status values are case sensitive.

The same `filter` argument works on most list tools in the connector, which is how you scope materials, activities, contacts or attachments to one job:

```
servicem8_list_job_materials     filter: "job_uuid eq '<uuid>'"
servicem8_list_job_activities    filter: "job_uuid eq '<uuid>'"
servicem8_list_attachments       filter: "related_object_uuid eq '<uuid>'"
```

## Semantic search, for vague asks

When the customer describes a job rather than identifying it ("the hot water job at the place in Newstead where the gate was locked"), `servicem8_search_jobs_semantic` will find it and a `$filter` will not.

```
servicem8_search_jobs_semantic   query: "hot water unit replacement, access problem, Newstead"
```

## General search across record types

`servicem8_search` and `servicem8_search_by_type` search across clients, jobs and contacts at once. Use this when you do not yet know which record type holds the answer, typically when starting from a phone number or a person's name.

## Aggregating

For "how many", "how much" or "which of these", load the filtered set and aggregate it rather than reading records one by one. A count is a count, not thirty tool calls.

If a result comes back truncated, treat it as truncated. Do not report a capped list as a complete one, and narrow the filter instead.

## Archived jobs

ServiceM8 soft deletes by setting `active` to 0. Archived jobs are still returned by the API and still visible in list results, so if a count looks too high, check whether you are including inactive records. `active` is not writable, so nothing archived can be restored here.
references/gotchas.md 4.8 KB
# Verified ServiceM8 traps

Every item here was confirmed against a live ServiceM8 account, not inferred from documentation. Where the documentation disagrees, the documentation is wrong.

## Writes that succeed and do nothing

The dangerous class: `errorCode 0`, `"message": "OK"`, a plausible `recordUuid`, and no record.

| Call | What happens |
|---|---|
| `servicem8_create_feedback` / `update_feedback` | Create returns OK with a UUID. Getting that UUID returns `Record Not Found`, and it never appears in a list. The whole feedback write surface is a no op |
| `servicem8_create_staff_message` | Persists only when `from_staff_uuid` and `to_staff_uuid` are the connected app's own staff record. Set either to a real person and the row is silently dropped |
| `servicem8_update_form` with `document_template_uuid` | Returns OK. The read back drops the key entirely. A form cannot be linked to its Word template through the API |
| `create_job` / `update_job` with `created_by_staff_uuid` | Always ignored on both verbs. Records are stamped with the app's staff identity |
| `servicem8_create_staff` with `color` or `security_role_uuid` | Both dropped. `color` comes back randomly assigned, `security_role_uuid` is absent from the read back |

**The rule this implies:** after any create or update that matters, read the record back. A `recordUuid` is not proof a row exists.

## Free text fields that are really flags

Both collapse anything other than `"1"` to `"0"`, so a sentence becomes a boolean and nobody notices.

| Field | Documented as | Actually |
|---|---|---|
| `client.billing_attention` | `string` | `0`/`1` toggle |
| `note.action_required` | "optional follow up action description" | `0`/`1` toggle |

## Required in practice, optional in the schema

| Call | Hidden requirement |
|---|---|
| `servicem8_create_job_allocation` | `allocation_window_uuid` is mandatory. The schema requires only `job_uuid` |
| `servicem8_create_job_material` | `displayed_amount` is required even when `material_uuid` is supplied |
| `servicem8_create_form` | `badge_name` is required. Omitting it fails with "Badge Name must be less than 12 characters", an error about length when the complaint is absence. The 12 character limit is real but inclusive, so a 12 character value is accepted |

## Undocumented side effects

| Trigger | Effect |
|---|---|
| `create_task` | Also creates a JobChecklist row named `"<task name>\n<task_details>"`. `delete_task` does not remove it. It orphans on the job forever |
| `create_job_contact` | Also creates a CompanyContact on the client, flagged primary. `delete_job_contact` does not remove it |
| `produce_document` with `templateType: "Invoice"` | Stamps the job's `invoice_date`. Rendering a document mutates the job |
| `produce_document` with `storeToDiary: true` | Stamps `invoice_date` **whatever the template type**, so filing a Quote to the diary marks the job invoiced. Treat any `storeToDiary` call as a billing action |
| `update_location` | Re-geocodes and overwrites explicit `lat`/`lng` even when you do not send them. `create_location` preserves what you send |
| `create_task` / `update_task` | `due_date` drifts forward one day per write. A repeatedly updated task walks its due date into the future indefinitely |

## Read and write shapes differ

Do not assert a wire type from the write schema.

| Field | Writes as | Reads as |
|---|---|---|
| `job.badges` | array of UUIDs | JSON string, `"[\"uuid\"]"` |
| `job.quote_sent`, `job.invoice_sent` | `0` / `1` | boolean `false` |
| `form.template_fields` | array | `false` when empty |
| `job.related_knowledge_articles` | string | `false` when empty, else an array of objects |
| `job_checklist.reminder_data` | JSON string | `[]` or a parsed object |

ServiceM8 also transliterates non-ASCII. An em dash written into `job_description` reads back as `--`.

## Things the API simply cannot do

Say so rather than working around them badly.

- **Restore anything.** Deletes set `active` to 0 and `active` is not writable on any resource
- **Write job custom fields.** `customfield_*` values are readable and writable by nothing
- **Produce anything but PDF.** `produce_document` advertises `pdf`, `docx` and `jpg`. Only `pdf` works, the other two return `Invalid Output Format`
- **Build a usable document template.** `create_document_template` sets only `name`, and `template_type` and `related_object` are read only, so the result renders nothing
- **Mark a payment as a deposit.** `job_payment.is_deposit` is readable, absent from the write schema
- **Edit a staff message.** `message` and `read_timestamp` are both rejected after creation

## Tax rates

`update_tax_rate` is not guarded and will rename or re-percentage an account wide rate that a synced accounting package depends on. `delete_tax_rate` is one way. Neither should be fired without an explicit, specific instruction.
references/job-fields.md 3.0 KB
# Job fields, and which ones are real

The writable set, grouped by what it is for. Anything not listed here is either read only or vendor computed, and sending it is silently discarded.

## Identity and description

| Field | Notes |
|---|---|
| `company_uuid` | The client. Required in practice |
| `status` | `Quote` / `Work Order` / `Completed` / `Unsuccessful`. Case sensitive |
| `job_address` | Free text. Geocoded by ServiceM8 |
| `billing_address` | Only if it differs from the client's |
| `job_description` | What needs doing. Read by field staff |
| `work_done_description` | What was done. Read by the customer on the invoice |
| `purchase_order_number` | The customer's PO, if they use them |
| `date` | The job date |

## Classification and workflow

| Field | Notes |
|---|---|
| `category_uuid` | Reporting category, from `servicem8_list_categories` |
| `badges` | Array of badge UUIDs on write. Reads back as a JSON **string** |
| `queue_uuid` | Dispatch queue placement |
| `queue_assigned_staff_uuid` | Who owns it inside that queue |
| `queue_expiry_date` | When it falls out of the queue |

## Billing state

| Field | Notes |
|---|---|
| `ready_to_invoice`, `ready_to_invoice_stamp` | |
| `invoice_sent`, `invoice_sent_stamp` | `invoice_sent` reads back as boolean `false`, writes as `0`/`1` |
| `quote_sent`, `quote_sent_stamp` | Same asymmetry |
| `payment_date`, `payment_method`, `payment_amount`, `payment_note` | |
| `payment_processed`, `payment_received` | Derived. See the warning below |
| `payment_actioned_by_uuid` | |

## Status timestamps

`quote_date`, `work_order_date`, `completion_date`, `unsuccessful_date`.

ServiceM8 stamps all four automatically when `status` changes. They are writable purely so a migration can backdate history. **Do not set them to move a job's state.** Set `status`.

## Geocoder output

`lat`, `lng`, `geo_is_valid`, `geo_country`, `geo_postcode`, `geo_state`.

Recomputed from `job_address` on every write. Set them only to correct a genuinely wrong geocode.

## Not writable, and worth knowing

- `generated_job_id` (the human job number). Read only. You cannot choose it
- `total_invoice_amount`. Computed from materials and activities
- `active`. ServiceM8 documents no resource as accepting it, so an archived job cannot be un archived through the API
- `created_by_staff_uuid`. Accepted by the schema, **always ignored**. Every record is stamped with the connected app's own staff identity
- `customfield_*`. An account's job custom fields are readable but writable by no tool. If a customer relies on custom fields, this is the most visible gap in the connector and you should tell them rather than pretend the write worked

## `payment_received` can flip the wrong way

It is derived, not stored. Writing adjacent payment fields can move it in a direction you did not intend. If payment state matters, read it back and check it rather than trusting the write.

## Contact details are not here

There is no phone or email field on the job. They live on `jobcontact` and sync inward. See [attach-to-a-job.md](attach-to-a-job.md).
references/materials-and-labour.md 3.1 KB
# Costing a job: materials and labour

Two separate resources. Parts go on `jobmaterial`, time goes on `jobactivity`. The job's `total_invoice_amount` is computed from them and is not writable.

## Materials, for parts and products

Two ways to add a line.

**From the account's price book:**

```
servicem8_create_job_material
  job_uuid: "<uuid>"
  material_uuid: "<uuid from servicem8_list_materials>"
  quantity: 1
  displayed_amount: 890.00
```

**Ad hoc, not in the price book:**

```
servicem8_create_job_material
  job_uuid: "<uuid>"
  name: "Rheem 250L electric HWS"
  quantity: 1
  displayed_amount: 890.00
  cost: 612.00
```

**`displayed_amount` is required on create even when you supply `material_uuid`.** The documentation implies it is only needed for ad hoc lines. It is not. Supplying the material UUID does not make ServiceM8 look the price up for you on create. `servicem8_update_job_material` does not require it.

`displayed_amount` is the sell price the customer sees. `cost` is what you paid, and drives margin reporting.

## Bundles, for repeated groups of parts

If the account uses bundles, `servicem8_create_job_material_bundle` adds a whole kit in one call. Faster and less error prone than ten material lines, and the account's pricing stays consistent.

```
servicem8_list_bundles
servicem8_create_job_material_bundle   job_uuid: "<uuid>"   bundle_uuid: "<uuid>"
```

## Activities, for labour and time on site

This is the timesheet layer. Every hour a staff member books to a job is a job activity.

```
servicem8_create_job_activity
  job_uuid: "<uuid>"
  staff_uuid: "<uuid>"
  start_date: "2026-09-15 08:00:00"
  end_date: "2026-09-15 11:30:00"
  activity_was_scheduled: 1
  activity_was_automatic: 0
```

Reading activities back is how you answer "how many hours did we spend on this job" and "what did this customer cost us in labour last quarter". `servicem8_list_job_activities` with a `$filter` on `job_uuid` or a date range.

For a labour cost question across many jobs, pull the activities and aggregate them rather than reading job by job. Do not try to compute this from the job record: the job does not carry hours.

## Tax rates: read, do not write

`servicem8_list_tax_rates` is safe.

`servicem8_create_tax_rate` is refused on accounts synced to an accounting package, with a clear error.

`servicem8_update_tax_rate` is **not** guarded, and it will rename or re-percentage an account wide tax rate that a synced accounting package depends on. There is no warning. Treat it as off limits unless the customer has explicitly asked for that exact change.

`servicem8_delete_tax_rate` archives a real account wide record and cannot be undone. Do not fire it.

## A margin check

To answer "did we make money on this job", you need three reads:

1. `servicem8_list_job_materials` filtered to the job, sum `cost` and `displayed_amount`
2. `servicem8_list_job_activities` filtered to the job, sum the durations and apply the account's charge out rate
3. `servicem8_get_job` for `total_invoice_amount`

Materials margin is directly available. Labour margin needs a rate the API does not hold, so ask for it rather than inventing one.
assets/example-payloads.json 2.4 KB
{
  "_comment": "Copy-ready shapes for the common ServiceM8 job calls. Replace every <uuid>. Field names are exact; ServiceM8 silently drops anything it does not recognise.",

  "create_client": {
    "name": "Acme Property Group",
    "address": "12 Wharf Road, Newstead QLD 4006",
    "abn_number": "51824753556"
  },

  "create_job_work_order": {
    "company_uuid": "<client uuid>",
    "status": "Work Order",
    "job_address": "12 Wharf Road, Newstead QLD 4006",
    "job_description": "Replace failed hot water unit, 250L electric. Access via rear gate, key in lockbox 4417.",
    "category_uuid": "<category uuid>",
    "purchase_order_number": "PO-88231"
  },

  "create_job_quote": {
    "company_uuid": "<client uuid>",
    "status": "Quote",
    "job_address": "12 Wharf Road, Newstead QLD 4006",
    "job_description": "Quote requested: replace 250L electric HWS, like for like."
  },

  "create_job_contact_site": {
    "job_uuid": "<job uuid>",
    "type": "JOB",
    "first": "Dana",
    "last": "Whitfield",
    "mobile": "+61400111222",
    "email": "dana@example.com"
  },

  "create_job_contact_billing": {
    "job_uuid": "<job uuid>",
    "type": "BILLING",
    "first": "Accounts",
    "last": "Payable",
    "email": "ap@example.com"
  },

  "update_job_classify": {
    "uuid": "<job uuid>",
    "category_uuid": "<category uuid>",
    "badges": ["<badge uuid>", "<badge uuid>"],
    "queue_uuid": "<queue uuid>",
    "queue_assigned_staff_uuid": "<staff uuid>"
  },

  "create_job_allocation": {
    "job_uuid": "<job uuid>",
    "staff_uuid": "<staff uuid>",
    "allocation_window_uuid": "<window uuid, MANDATORY>",
    "allocation_date": "2026-09-15"
  },

  "create_job_material_from_pricebook": {
    "job_uuid": "<job uuid>",
    "material_uuid": "<material uuid>",
    "quantity": 1,
    "displayed_amount": 890.00
  },

  "create_job_material_ad_hoc": {
    "job_uuid": "<job uuid>",
    "name": "Rheem 250L electric HWS",
    "quantity": 1,
    "displayed_amount": 890.00,
    "cost": 612.00
  },

  "create_job_activity_labour": {
    "job_uuid": "<job uuid>",
    "staff_uuid": "<staff uuid>",
    "start_date": "2026-09-15 08:00:00",
    "end_date": "2026-09-15 11:30:00",
    "activity_was_scheduled": 1
  },

  "complete_job": {
    "uuid": "<job uuid>",
    "work_done_description": "Removed and disposed of failed 250L unit. Installed new Rheem 250L electric, tested, no leaks. Isolation valve replaced.",
    "status": "Completed",
    "ready_to_invoice": 1
  }
}

Questions, answered

What does the Manage jobs in ServiceM8 skill do?

Read this before creating, updating, scheduling, costing or completing a job in ServiceM8, or before adding anything to a job (contacts, badges, materials, labour, notes, payments). It is a document in the Agent Skills format: the steps, the rules and the reference files your AI reads when the job comes up. It is written for ServiceM8, and installs into any workspace whether or not those are connected.

How do I install it?

Add to FloConnector opens it inside your workspace, where Install puts it into one of your collections. Every profile carrying that collection has it on its next call. Download zip gives you the same skill as a bundle for any client that installs skills from disk.

Will it change after I install it?

Only if you ask it to. Keep updated follows FloConnector's revisions (this is v1) and records each one in the skill's history. Make my own is a copy that never changes unless you change it, and a kept-updated skill can be made editable later in one click.

Can I edit it or reuse it elsewhere?

Yes. You can copy, change, rename and redistribute it, commercially or not, with no attribution. Every skill in the library is published under CC0 1.0, and the zip carries the licence text.