Package Information
Documentation
@thegraphitelab/n8n-nodes-servicetitan
n8n custom node package for ServiceTitan. Built by The Graphite Lab, published publicly on npmjs.org under the @thegraphitelab scope. Primary consumer is TGL's Railway-hosted n8n fleet; the package is openly browsable.
Current version: 0.6.0
Installation
In each n8n instance, go to Settings → Community Nodes → Install and enter:
@thegraphitelab/n8n-nodes-servicetitan
Accept the disclaimer and install.
Required env var
Set on the n8n service (both primary and worker if running queue mode):
TGL_SERVICETITAN_APP_KEY=<TGL ServiceTitan ST-App-Key>
This is the TGL-owned ST-App-Key from our ServiceTitan developer portal app — same across all TGL clients, sourced from env so the value never sits in client-visible credential fields. See the repo root README for the broader rationale.
What ships today
Credential: ServiceTitan API
- OAuth 2.0
client_credentialsgrant ST-App-Keyheader sourced fromTGL_SERVICETITAN_APP_KEY- Environment switch:
Integration (Sandbox)vsProduction preAuthenticationcaches anexpirableaccessTokenin the credential data (15min TTL)- Test endpoint:
/settings/v2/tenant/{tenantId}/user-roles?page=1&pageSize=1
UI fields (per-client, set when creating the credential in n8n):
- Environment —
Integration (Sandbox)orProduction - Tenant ID — from ServiceTitan → Settings → API Application Access
- Client ID — OAuth client ID, per-client
- Client Secret — OAuth client secret, per-client
Action node: ServiceTitan
Picker label: ServiceTitan. Internal n8n name: tglServiceTitan.
Resource + Operation pattern.
| Resource | Operations |
|---|---|
| Customer | Get by ID |
The Customer.Get response includes both raw customFields[] and a flattened customFieldsByName sibling object (see Custom fields below).
Trigger node: ServiceTitan Trigger
Picker label: ServiceTitan Trigger. Sits next to the action node in n8n's node picker when the user searches "ServiceTitan".
Two-level Resource + Event picker inside the node, matching the action node's pattern. Resource determines which Event options show.
| Resource | Events |
|---|---|
| Job | Created or Modified |
Polling-based with modifiedOnOrAfter cursor and a boundary-id dedup set to avoid duplicate emissions on overlap windows.
Architecture
Manifest-driven. The shared engine lives in @thegraphitelab/n8n-nodes-core (a runtime dependency, pulled in automatically when n8n installs this package); this package supplies only ServiceTitan's specifics:
nodes/ServiceTitan/v1/generated/manifests.ts— the generated manifest: every resource/operation and trigger, each described declaratively by ashape. Build output fromn8n-codegen— do not hand-edit.nodes/ServiceTitan/v1/transport/config.ts— theTransportConfigthe engine reads: credential key, base URL, pagination/cursor rules, andpathContext(maps the credential'stenantIdinto the{tenant}path placeholder).nodes/ServiceTitan/v1/*.node.ts+descriptions/— the action and trigger node wrappers, which call the engine'srouter/pollRouterandgetActionProperties/getTriggerProperties.credentials/— the ServiceTitan API credential.
The engine compiles the manifest into n8n operations, poll triggers, property descriptions, and the authenticated HTTP transport. Adding or refreshing coverage is a codegen regenerate + rebuild (see the repo root README), not a hand-edit.
Custom fields behavior
ServiceTitan returns custom fields as an array of {typeId, name, value} objects. That shape is awkward to consume in downstream n8n nodes — every workflow has to write a JS-code step to find a field by name.
This package returns three shapes on every entity that has custom fields, so consumers pick what fits:
customFields— raw array of{typeId, name, value}, untouched. Use this if your workflow already depends on the ST shape, or you need both name + typeId together.custom_fields_by_name— flat map: keys are field names, values are the raw values. Use{{ $json.custom_fields_by_name["Lockbox Code"] }}to read directly.custom_fields_by_type_id— flat map: keys are stringified typeIds, values are the raw values. Use this when you want to address a field by ID (more stable than name across tenant renames).
All three coexist on every entity. None is destructive of the others.
Roadmap
Phase 1 (current): refactored framework that scales, shipping the existing Customer.Get + Jobs trigger through it.
Phase 3: port the rest of TGL's ServiceTitan Zapier operations resource-by-resource. Reference source: /Users/jacobnolley/projects/advanced_servicetitan_zapier_app (the live Zapier custom app).
Resources to port (non-exhaustive):
- Jobs (more operations beyond the trigger)
- Customers (full CRUD)
- Estimates
- Appointments
- Invoices
- Projects
- Payments
- Calls
- Leads
- Locations
- Technicians
- Vendors
- Employees
- Bookings
Each resource lands as a minor bump; each new operation on an existing resource is a patch.
Local dev
# From this directory
pnpm dev
Boots embedded n8n at http://localhost:5678 with this package pre-loaded and hot reload. ServiceTitan sandbox credentials go in the local n8n credential store (~/.n8n).
Reference
- Repo root:
../../README.md - Recipes for adding resources, trigger events, new packages:
../../CONTRIBUTING.md - Source-of-truth Zapier app being ported from:
/Users/jacobnolley/projects/advanced_servicetitan_zapier_app