Schedule & crews
Read and build the crew schedule, schedule won work from the queue, move and staff visits, publish to crews, and manage crews and the people on them.
View as MarkdownThe schedule is made of work orders: one scheduled visit of a crew to a job. A visit has a start date, a length in working days, an optional start and finish time, a crew, and the people and equipment on it. A job can have many visits (phases, return trips, repeat service).
These endpoints run the same rules as the Schedule page in TruTec: crew-day locks, double-booking checks, skipped days and work weeks, and the undo history all apply.
How the schedule works#
Edits are drafts until you publish. Creating, moving or re-crewing a visit changes the office schedule only. Crews see the change on their phones after POST /v1/schedule/publish, the same as pressing Publish in TruTec. Publishing is a one-way step: it sends the visits to crews and cannot be taken back. Moving a visit that was already published makes a new draft change that the next publish sends.
Who the API acts as. Writes are made as the person who created the API key. Their own TruTec permissions apply: if they can only view the schedule, schedule writes return 403 view_only. Changes show in TruTec's history under their name. If that person has left the workspace, been suspended or switched to another workspace, writes return 403 Key owner unavailable; create a new key. Partner keys act as the workspace owner.
Operations must be on. Scheduling, crews and timesheets are part of TruTec's operations tools. If they are not turned on for the workspace, writes return 403 Operations not enabled.
Dates are YYYY-MM-DD and times are 24-hour local times ("07:30") in the workspace's time zone. Timestamps are UTC with a Z suffix.
Note
Partner keys that act on behalf of several workspaces must also pass firm_id (query string on reads, JSON body on writes).
The work order object#
| Field | Type | Description |
|---|---|---|
id |
integer | Work order id. |
job_id, job |
integer, object | The job, with {id, name}. |
title |
string | What the crew sees, for example "Sealcoat front lot". |
status |
string | unscheduled, scheduled, in_progress, completed or cancelled. |
is_hold |
boolean | A tentative "hold a date" block. Holds never publish. |
scheduled_date |
string or null | First working day. Null while unscheduled. |
work_dates |
array | Every working day the visit covers, after skipped days, days off and the crew's work week. |
estimated_days |
number or null | Length in working days. |
start_time, end_time |
string or null | Local times. ends_next_day is true for a finish after midnight. |
skip_dates |
array | Days this visit skips. |
work_weekdays |
array | Weekdays the visit may land on, 1 (Monday) to 7 (Sunday). Empty means any working day. |
crew_id, crew |
integer, object | The crew, with {id, name}. |
foreman |
object or null | {id, name}. |
people |
array | Who is on the visit: the crew's members plus or minus people moved on or off it. Each has a person_key and name. |
quantity, unit |
number, string | How much work, for example 42000 SF. |
completed_quantity, completed_unit, completion_note |
What the crew reported. | |
scope, service_types |
string, array | Scope text and the types of work folded into this visit. |
address, latitude, longitude |
Where the crew goes. | |
notes, gate_code, staging_notes |
string or null | Dispatch notes for the crew. |
color, sequence |
string, integer | Calendar color and the order within the crew's day. |
equipment |
array | {id, name} for each machine on the visit. |
estimate_id, estimate_service_id |
integer or null | The estimate line the visit was scheduled from. |
customer_notify |
string or null | none, email, sms or both. Null follows the workspace default. |
customer_notified_at, customer_confirmed_at |
string or null | When the customer was told, and when they confirmed. |
dispatched_at, completed_at |
string or null | When the packet was sent, and when the visit was finished. |
has_unpublished_changes |
boolean | The next publish would send this visit. |
created_at, updated_at |
string | UTC timestamps. |
Work orders#
List work orders#
GET /v1/work-orders
Permission: Read jobs
Returns visits ordered by date (unscheduled last). With a date range, a multi-day visit is included when any of its working days falls in the range. With updated_after, results are sorted by updated_at, oldest first.
| Name | Type | Required | Description |
|---|---|---|---|
start_date, end_date |
string | No | Date range, up to 366 days. |
crew_id |
integer | No | Only this crew's visits. |
unassigned |
boolean | No | true for visits with no crew. |
job_id |
integer | No | Only this job's visits. |
status |
string | No | One status, or several separated by commas. |
include_holds |
boolean | No | Include "hold a date" blocks. Default false. |
updated_after |
string | No | ISO 8601. Only visits changed after it. |
limit, offset |
integer | No | Page size (default 50, maximum 200) and rows to skip. |
curl "https://trutec.ai/api/v1/work-orders?start_date=2026-11-02&end_date=2026-11-08&crew_id=18" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [
{
"id": 4120,
"job_id": 70412,
"job": { "id": 70412, "name": "Northgate Plaza reseal" },
"title": "Sealcoat front lot",
"status": "scheduled",
"is_hold": false,
"phase": null,
"scheduled_date": "2026-11-03",
"work_dates": ["2026-11-03", "2026-11-04"],
"estimated_days": 2.0,
"start_time": "07:00",
"end_time": "15:30",
"ends_next_day": false,
"skip_dates": [],
"work_weekdays": [],
"crew_id": 18,
"crew": { "id": 18, "name": "Seal Crew 1" },
"foreman": { "id": 344, "name": "Crew Lead" },
"people": [
{ "person_key": "person:51", "name": "Crew Lead" },
{ "person_key": "name:laborer 2", "name": "Laborer 2" }
],
"quantity": 42000.0,
"unit": "SF",
"completed_quantity": null,
"completed_unit": null,
"completion_note": null,
"scope": "Clean, crack fill and two coats of sealer on the front lot.",
"service_types": [],
"address": "1200 Commerce Dr, Austin, TX 78701",
"latitude": 30.2711,
"longitude": -97.7437,
"notes": "Gate opens at 6:45",
"gate_code": "4412",
"staging_notes": "Stage the rig behind building C",
"color": null,
"sequence": 0,
"equipment": [{ "id": 902, "name": "Seal rig 2" }],
"estimate_id": 9840,
"estimate_service_id": 55120,
"customer_notify": "email",
"customer_notified_at": "2026-10-28T16:02:11Z",
"customer_confirmed_at": null,
"dispatched_at": null,
"completed_at": null,
"created_at": "2026-10-20T15:44:02Z",
"updated_at": "2026-10-28T16:02:11Z",
"has_unpublished_changes": false
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0 }
}
Errors: 400 for a bad date, a range over 366 days, or an unknown status.
Get a work order#
GET /v1/work-orders/{id}
Permission: Read jobs
Returns one work order. A work order in another workspace returns 404 Work order not found.
Create a work order#
POST /v1/work-orders
Permission: Write jobs
Adds a visit. There are two ways:
- New work on a job: send
job_idandtitle. - Queued work from a won estimate: send a
queue_idfrom List the queue (orestimate_service_id). TruTec builds the visit from the estimate line (title, quantity, scope, equipment) exactly as when you drag a queue card onto the board. The job must be in a stage that can be scheduled.
Add crew_id and scheduled_date to place it at the same time; without them the visit waits in the unscheduled queue. The visit is a draft until you publish.
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
integer | Yes, unless queue_id |
The job. |
title |
string | Yes, unless queue_id |
What the crew sees. |
queue_id |
string | No | From the queue, for example es-55120 or job-70412. |
crew_id |
integer | No | One of the workspace's active crews. |
scheduled_date |
string | No | First working day. |
estimated_days |
number | No | 0.5 to 365. |
work_dates |
array | No | Exact working days instead of a start date and length (1 to 120 dates). |
start_time, end_time |
string | No | Local times. A finish at or before the start ends the next day. |
phase, notes, unit, gate_code, staging_notes |
string | No | Details for the crew. |
color |
string | No | #rrggbb. |
foreman_user_id |
integer | No | A user in the workspace. |
customer_notify |
string | No | none, email, sms or both. Alerts go out when the visit is published. |
equipment_ids |
array | No | Equipment ids from your catalog. |
skip_dates |
array | No | Days to skip. |
work_weekdays |
array | No | 1 (Monday) to 7 (Sunday). |
curl -X POST "https://trutec.ai/api/v1/work-orders" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"job_id": 70412,
"title": "Sealcoat front lot",
"crew_id": 18,
"scheduled_date": "2026-11-03",
"estimated_days": 2,
"start_time": "07:00",
"notes": "Gate opens at 6:45"
}'
Returns 201 with the work order (see the object above).
Errors:
400for a missingtitleorjob_id, a malformed value, or an unknown field.404when the job, crew or queue item is not in your workspace.409crew_day_lockedwhen that crew day is locked, or a crew rule (for example a required co-worker) blocks the assignment; the response lists thelocksorviolations.- If the visit was created but could not be placed, the error includes
work_order_id: the visit exists in the queue, so fix the request andPATCHit instead of creating another.
Update or reschedule a work order#
PATCH /v1/work-orders/{id}
Permission: Write jobs
Changes only the fields you send. To move a visit, send a new scheduled_date and, if it changes, crew_id. Send "scheduled_date": null to return it to the unscheduled queue. A move of a published visit can carry a reason (for example "Rain"), which TruTec keeps with the change.
Accepts every field from Create except job_id and queue_id, plus reason. Status is not editable here; use the actions below.
curl -X PATCH "https://trutec.ai/api/v1/work-orders/4120" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "scheduled_date": "2026-11-05", "reason": "Rain" }'
Returns 200 with the updated work order. Errors are the same as Create.
Delete a work order#
DELETE /v1/work-orders/{id}
Permission: Write jobs and Delete records
Removes a visit and frees its crew days. Returns 204. A visit with recorded crew time or workflow history returns 409 workflow_history_recorded; cancel it instead.
Complete, reopen, cancel or unschedule#
POST /v1/work-orders/{id}/complete
POST /v1/work-orders/{id}/reopen
POST /v1/work-orders/{id}/cancel
POST /v1/work-orders/{id}/unschedule
Permission: Write jobs
| Action | What it does |
|---|---|
complete |
Marks the visit complete. Send {"whole_job": true} to finish every open visit on the job; the response lists the others in also_completed_ids. |
reopen |
Puts a completed visit back to scheduled (or unscheduled if it has no date). |
cancel |
Cancels the visit. If the customer was already told about it, they get a cancellation alert, as in TruTec. |
unschedule |
Returns the whole visit to the queue. Send {"date": "2026-11-04"} to drop only that working day. |
Each returns 200 with the work order. Completed visits and holds cannot be unscheduled (400).
Put a person on a visit, or take them off#
POST /v1/work-orders/{id}/people
Permission: Write jobs
Moves one person onto or off a visit without changing the crew, the same as dragging a person on the Dispatch board. Someone already on another visit that day is moved off it (moved_from_work_order_id). By default the change also applies to later visits of the same estimate line (future_work_orders_changed); send carry_forward: false for this visit only. People changes reach crews right away and do not need a publish.
| Name | Type | Required | Description |
|---|---|---|---|
action |
string | Yes | assign or remove. |
user_id |
integer | One of these | A crew member with a TruTec login. |
person_key |
string | One of these | From List people or the visit's people. |
date |
string | No | The day you are changing, for a multi-day visit. Default today. |
just_this_day |
boolean | No | Change only date on a multi-day visit. |
carry_forward |
boolean | No | Default true. |
curl -X POST "https://trutec.ai/api/v1/work-orders/4120/people" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "action": "assign", "user_id": 377, "date": "2026-11-03" }'
{
"data": { "id": 4120, "people": [ { "person_key": "person:51", "name": "Crew Lead" }, { "person_key": "name:pat rivera", "name": "Pat Rivera" } ] },
"changed": true,
"scope": "visit",
"future_work_orders_changed": 0
}
(data is the full work order; shortened here.)
Queue and publishing#
List the queue#
GET /v1/schedule/queue
Permission: Read jobs
Work that is sold but not on the schedule yet: unscheduled work orders, plus estimate lines from won jobs that have no visit yet. Schedule a queue item with POST /v1/work-orders and its queue_id; an item that is already a work order has a work_order_id instead, so PATCH it.
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
integer | No | Only this job's work. |
limit, offset |
integer | No | Paging. |
{
"data": [
{
"queue_id": "es-55120",
"work_order_id": null,
"kind": "estimate_service",
"job_id": 70412,
"job": { "id": 70412, "name": "Northgate Plaza reseal" },
"title": "Sealcoating",
"quantity": 42000.0,
"unit": "SF",
"estimated_days": 2.0,
"suggested_crew_id": 18,
"estimate_id": 9840,
"estimate_service_id": 55120,
"service_types": []
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0 }
}
kind is estimate_service, task, job (a won job with no estimate lines) or work_order.
Get publish status#
GET /v1/schedule/publish-status
Permission: Read jobs
What the next publish would send: visits in the next 30 days with draft changes.
{
"data": {
"status": "draft",
"unpublished_count": 2,
"unpublished_work_order_ids": [4120, 4133],
"unpublished_jobs": [{ "job_id": 70412, "work_order_count": 2 }],
"last_publication": {
"id": 812,
"published_at": "2026-10-30T21:14:05Z",
"published_by": { "id": 12, "name": "Office Manager" },
"note": "Week of Nov 2",
"scope": "all",
"job_id": null,
"work_order_count": 14,
"crew_notified_count": 9
}
}
}
Publish the schedule#
POST /v1/schedule/publish
Permission: Write jobs
Sends draft changes to crews, exactly like Publish in TruTec. This reaches people: crew members get an in-app notification for their visits, and customers get the email or text alert set on each visit (customer_notify). Publishing cannot be undone.
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
integer | No | Publish only this job's visits. Other drafts stay drafts. |
work_order_ids |
array | No | Publish only these visits. |
note |
string | No | Shown in the schedule's publish history. |
curl -X POST "https://trutec.ai/api/v1/schedule/publish" \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{ "note": "Week of Nov 2" }'
{
"data": {
"published": true,
"published_count": 14,
"crew_notified_count": 9,
"customers_alerted_count": 3,
"publication": { "id": 813, "published_at": "2026-11-01T18:00:12Z", "published_by": { "id": 12, "name": "Office Manager" }, "note": "Week of Nov 2", "scope": "all", "job_id": null, "work_order_count": 14, "crew_notified_count": 9 }
}
}
With nothing to send, published is false and the counts are 0.
Schedule notes#
Short notes on a date that are not tied to a job ("Safety meeting 6:30am", "Plant closed"), for the whole workspace, one crew or one foreman.
List schedule notes#
GET /v1/schedule/notes?start_date=2026-11-02&end_date=2026-11-08
Permission: Read jobs
start_date and end_date are required (at most 120 days apart). Optional crew_id.
{
"data": [
{
"id": 301,
"date": "2026-11-03",
"text": "Safety meeting 6:30am",
"color": "amber",
"crew": { "id": 18, "name": "Seal Crew 1" },
"foreman": null,
"created_at": "2026-10-30T14:02:00Z",
"updated_at": "2026-10-30T14:02:00Z"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0 }
}
Create, update or delete a schedule note#
POST /v1/schedule/notes
PATCH /v1/schedule/notes/{id}
DELETE /v1/schedule/notes/{id}
Permission: Write jobs (delete also needs Delete records)
| Name | Type | Required | Description |
|---|---|---|---|
date |
string | Yes on create | The day. |
text |
string | Yes on create | Up to 140 characters. |
color |
string | No | blue, navy, amber, red, purple or slate. |
crew_id |
integer | No | One crew. |
foreman_user_id |
integer | No | One foreman. Not with crew_id. |
Create returns 201, update 200, delete 204.
Crews#
The crew object#
| Field | Type | Description |
|---|---|---|
id, name |
||
is_active |
boolean | Archived crews are false and can't be scheduled. |
group_name, description |
string or null | From your crew catalog. |
headcount, hours_per_day, cost_per_day, working_days_per_week |
number | Crew size and cost used by estimating and capacity. |
role_slots |
array | The crew's line-up of roles, in order, for example ["Paver", "Screed", "Roller"]. |
members |
array | The people on the crew (see below). |
created_at, updated_at |
string | UTC timestamps. |
A member has id (the membership id), crew_id, user_id (null for an employee with no TruTec login), name, role (lead or member), work_role (their slot, for example Roller), work_classification and has_app_access.
List crews#
GET /v1/crews
Permission: Read jobs
Active crews in the order set on the Crews page. include_inactive=true adds archived crews. Also takes q (name contains), updated_after, limit and offset.
{
"data": [
{
"id": 18,
"name": "Seal Crew 1",
"is_active": true,
"group_name": "Sealcoating",
"description": null,
"headcount": 4,
"hours_per_day": 10.0,
"cost_per_day": 2480.0,
"working_days_per_week": 5,
"role_slots": ["Lead", "Squeegee", "Sprayer"],
"members": [
{
"id": 77,
"crew_id": 18,
"user_id": 344,
"name": "Crew Lead",
"role": "lead",
"work_role": "Lead",
"work_classification": null,
"has_app_access": true,
"created_at": "2026-08-02T15:10:00Z"
}
],
"created_at": "2026-03-11T19:20:41Z",
"updated_at": "2026-09-30T13:02:10Z"
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0 }
}
Get a crew#
GET /v1/crews/{id}
Permission: Read jobs
Create a crew#
POST /v1/crews
Permission: Write jobs. The key's creator must be a workspace owner.
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Up to 200 characters. |
role_slots |
array | No | Role names, up to 40. |
Returns 201. A crew with the same name (any case) is not duplicated: you get that crew back with 200, reactivated if it was archived.
Update a crew#
PATCH /v1/crews/{id}
Permission: Write jobs (owner)
Send name (1 to 120 characters) and/or role_slots (the full new line-up; an empty list clears it). Members keep their work_role when a slot goes away.
Archive a crew#
DELETE /v1/crews/{id}
Permission: Write jobs and Delete records (owner)
Archives the crew (history and past visits keep it). Returns 204. A crew with upcoming scheduled work returns 409; move that work first.
Add a crew member#
POST /v1/crews/{id}/members
Permission: Write jobs (owner)
| Name | Type | Required | Description |
|---|---|---|---|
user_id |
integer | One of these | A TruTec login in the workspace. |
ep_individual_id or ep_person_id |
integer | One of these | An employee from your estimating labor catalog. They join without a login. |
role |
string | No | lead or member (default). Logins only. |
work_role |
string | No | Their slot on the crew, for example Roller. |
Returns 201 with the member, or 200 if they were already on the crew. A crew rule that blocks the move returns 409 with violations.
Update or remove a crew member#
PATCH /v1/crews/{id}/members/{membership_id} with {"work_role": "Screed"} (blank clears it).
DELETE /v1/crews/{id}/members/{membership_id} (needs Delete records) takes the person off the crew. Returns 204.
People#
List people#
GET /v1/people
Permission: Read jobs
Everyone on any crew roster, one row per person, with the crews they sit on. person_key is the identity the schedule uses for people on visits. Filters: q (name contains), crew_id.
{
"data": [
{
"person_key": "person:51",
"name": "Crew Lead",
"user_id": 344,
"has_app_access": true,
"license": "cdl_b",
"skills": ["Screed", "Spanish"],
"crews": [
{ "id": 18, "name": "Seal Crew 1", "membership_id": 77, "role": "lead", "work_role": "Lead", "is_active": true }
]
}
],
"pagination": { "total": 1, "limit": 50, "offset": 0 }
}
To record time for someone, they need a TruTec login (user_id). See Timesheets. Workspace logins are listed by GET /v1/users.