Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.vh3.ai/llms.txt

Use this file to discover all available pages before exploring further.

Cursor Rules

Cursor reads .cursor/rules/*.mdc files automatically and applies them to chats based on the rule’s scope. Unlike AGENTS.md, which is conversational context, Cursor rules apply specifically to code generation — they shape what Cursor writes, not just what it says. Drop the three files below into .cursor/rules/ and any code Cursor generates in that project will use the right parameter names, handle the right errors, and follow the VH3 API conventions.
Use rules + AGENTS.md together. AGENTS.md tells the agent which endpoint to call for which question. Cursor rules ensure the code it writes around those calls is correct.

File 1 — vh3-api-domain.mdc (always apply)

This rule gives Cursor persistent VH3 vocabulary. It applies to every chat in the project.
---
description: VH3 AI API domain vocabulary, schemas, and field names. Always apply.
alwaysApply: true
---

# VH3 AI — Domain Model

All code in this project integrates with the VH3 AI intelligence layer.
Use these exact names and types when generating API calls, type
definitions, or data models.

## Base URL & auth

- Base URL: `https://api.vh3connect.io/api:kP8T1CK7`
- Every request requires `company_id` (string) and `api_key` (string)
- POST: pass in JSON body. GET: pass as query params.
- NEVER hardcode credentials. Read from `VH3_COMPANY_ID` and `VH3_API_KEY`
  environment variables.

## Entities

- `Job` — id: `jobId` (int), external ref: `reference` (string, e.g. "FAB303178")
- `Engineer` — id: `resourceId` (string), display: `name`
- `Site` — id: `siteKey` (string), display: `siteAddress`
- `Customer` — id: `contactId` (int), display: `name`
- `JobGroup` — id: `jobGroupId` (int), groups related visits
- `ParentCustomer` — id: name string (e.g. "Whitbread PLC")

## Job key fields

- `reference`, `jobId`, `companyId`
- `status` — completedOk, completedWithIssues, inProgress, notStarted
- `result` — "Job Complete", "Pricing required", "Parts required", "No access"
- `vertical` — security | fire_life_safety | mep | building_fabric |
  utilities | soft_services | it_comms | other
- Timing: `plannedStartAt`, `actualStartAt`, `startDeltaMins`, `endDeltaMins`
- `processedOutcome` — long-form text, always truncate before display

## CRITICAL — never invent parameter names

Use exactly these names (camelCase, NOT snake_case for filter fields):

`companyId` (NOT company_id in JSON bodies of v2 endpoints)
`resourceId` (NOT engineer_id, resource_id, engineerId)
`siteKey` (NOT site_id, site_key, siteId)
`contactId` (NOT customer_id, contact_id, customerId)
`typeId` (NOT type_id, jobTypeId)

⚠️ EXCEPTION: legacy v1 endpoints (e.g. /search/outcomes, /sentinels/run,
/connie/chat) use `company_id` and `api_key` (snake_case) in the body.
Check the OpenAPI spec when unsure: https://api.vh3connect.io/openapi.json

## Aggregation metrics (whitelisted)

POST /aggregate/jobs accepts only these metrics:
- `job_count`
- `completion_rate`
- `first_visit_fix_rate`
- `avg_start_delta_mins`
- `avg_end_delta_mins`

GroupBy: status, result, type, category, engineer, site, vertical,
day, week, month.

Period shorthand: today, yesterday, this_week, last_week, this_month,
last_month, last_7_days, last_30_days, last_90_days.

CompareTo: previous_period, same_period_last_week, same_period_last_month.

## timeAxis — CRITICAL default

Default `timeAxis = "actualStartAt"` for any retrospective question.
Other values: actualEndAt (completions), plannedStartAt (forward-looking
only — includes phantom jobs), createdAt (intake only), scheduledAt
(dispatching).

## Error envelope

All errors:
```
{ "code": "ERROR_CODE_...", "message": "...", "payload": {} }
```

Status codes: 400 validation, 401 unauthorised, 404 not found, 422
semantic validation, 429 rate limit, 500 server error. Retryable: 429,
502, 504. Not retryable: 400, 401, 404, 422.

## Identity in responses

Never expose `companyId`, `jobId`, `resourceId`, `siteKey`, `contactId`,
`typeId`, `categoryId` in user-facing output. Use names and references.

File 2 — vh3-api-calls.mdc (TypeScript / JavaScript / Python)

This rule applies when writing API integration code in .ts, .js, or .py files.
---
description: VH3 API call patterns — auth, timeouts, retries, error handling
globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.py"]
alwaysApply: false
---

# VH3 API — Code Patterns

When writing code that calls the VH3 API, follow these patterns.

## Auth — always from env

```ts
const VH3_BASE = "https://api.vh3connect.io/api:kP8T1CK7";
const VH3_COMPANY_ID = process.env.VH3_COMPANY_ID!;
const VH3_API_KEY = process.env.VH3_API_KEY!;
```

Throw a clear error if either is missing at module load — do not let
requests fail with 400 at runtime.

## Standard POST helper

```ts
async function vh3Post<T>(path: string, body: Record<string, unknown>): Promise<T> {
  const res = await fetch(`${VH3_BASE}${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      company_id: VH3_COMPANY_ID,
      api_key: VH3_API_KEY,
      ...body,
    }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Vh3Error(res.status, err.code, err.message);
  }
  return res.json() as Promise<T>;
}
```

## Timeouts

| Endpoint | Set timeout to |
|---|---|
| `/investigate` (with narrative) | 25s |
| `/reports/generate` (with narrative) | 25s |
| `/aggregate/jobs` | 10s |
| `/jobs/feed` | 10s |
| `/sentinels/run` | 15s |
| `/search/outcomes` | 10s |

The `/investigate` and `/reports/generate` endpoints with narrative have
10–17s expected latency. Do NOT set a 5s timeout.

## Retry policy

Retry on 429, 502, 504 with exponential backoff (1s, 2s, 4s, max 3 attempts).
Do NOT retry 400, 401, 404, 422 — they will not succeed.

## Pagination

`/jobs/feed` is paginated. Use `page_size` (default 20, max 200) and
`page_number`. Stop when returned array length < page_size.

## Period helpers — never compute date ranges manually

For `/aggregate/jobs`, use the `period` shorthand instead of computing
startDate/endDate. The server handles ISO week boundaries, partial
periods, and timezone correctness.

`{ period: "this_week", compareTo: "previous_period" }`
❌ Manually computing `startDate = new Date(...)` then passing as filter

## TypeScript types

When generating types for API responses, mark optional fields as such
(many endpoints return optional engineer/customer/site data). Use the
OpenAPI spec at `https://api.vh3connect.io/openapi.json` as the source
of truth — do not infer types from one example response.

File 3 — vh3-n8n-workflows.mdc (n8n workflows)

For projects using n8n-as-code to build VH3 workflows.
---
description: VH3 API patterns inside n8n workflow files
globs: ["**/*.workflow.ts"]
alwaysApply: false
---

# VH3 API — n8n Workflow Patterns

When generating n8n nodes that call the VH3 API, follow these patterns.

## Credentials, not hardcoded values

Create an n8n credential called `vh3Api` with two fields: `companyId`
and `apiKey`. Reference in nodes via `{{ $credentials.vh3Api.companyId }}`
and `{{ $credentials.vh3Api.apiKey }}`. Never put these in node parameters.

## HTTP Request node — VH3 POST pattern

```ts
@node({
  name: 'Run Sentinels',
  type: 'n8n-nodes-base.httpRequest',
  version: 4.2,
  position: [400, 200],
})
RunSentinels = {
  method: 'POST',
  url: 'https://api.vh3connect.io/api:kP8T1CK7/sentinels/run',
  sendBody: true,
  contentType: 'json',
  bodyParameters: {
    parameters: [
      { name: 'company_id', value: '={{ $credentials.vh3Api.companyId }}' },
      { name: 'api_key',    value: '={{ $credentials.vh3Api.apiKey }}' },
    ],
  },
  options: { timeout: 15000 },
};
```

## Common patterns

| Goal | Endpoint | Schedule trigger |
|---|---|---|
| Daily morning briefing | POST /reports/generate (start_of_day) | 07:00 weekdays |
| End-of-day debrief | POST /reports/generate (close_of_business) | 18:00 weekdays |
| Sentinel sweep + Slack alert | POST /sentinels/run → Slack node | hourly or daily |
| New-job semantic context | POST /search/outcomes triggered by FMS webhook | webhook |
| Weekly account report | POST /reports/generate (account_monthly) | Monday 09:00 |

## AI Agent node — use VH3 as a tool

When wiring an AI Agent node, register VH3 endpoints as HTTP Request Tools.
Always pass through `company_id` and `api_key` from credentials, and
include a tool description that tells the model when to use it.

See [n8n Agent Prompts](/agent-kits/n8n-agents) for ready-to-paste
system prompts and tool descriptions.

## NEVER

- ❌ Never hardcode `company_id` or `api_key` in workflow JSON
- ❌ Never use the HTTP Request node's "raw body" mode — use parameters
  so credentials show as `={{ $credentials... }}` not plaintext
- ❌ Never set HTTP timeout < 20000ms for /investigate or /reports/generate
- ❌ Never commit a workflow with a populated credential — push will reject

Installing the rules

1

Create the rules directory

mkdir -p .cursor/rules
2

Drop the three files in

Copy each block above into the corresponding filename inside .cursor/rules/.
3

Reload Cursor

Cursor reads .mdc files on project load. Reload the window or reopen the project to pick up changes.
4

Test it

Ask Cursor: “Write me a TypeScript function that calls the VH3 aggregate endpoint to get this week’s completion rate by engineer.” The generated code should use actualStartAt as the timeAxis, read auth from env vars, and use the correct camelCase parameter names.

How the rules interact

Domain rule (always)

vh3-api-domain.mdc applies to every chat. Cursor always knows the entity model and field names.

Code rule (.ts/.py)

vh3-api-calls.mdc applies only when editing implementation files. Shapes generated code.

n8n rule (.workflow.ts)

vh3-n8n-workflows.mdc applies inside n8n-as-code projects. Enforces credential and timeout conventions.

AGENTS.md (conversation)

AGENTS.md handles routing and reasoning. Cursor rules handle code-level correctness. Use both.
The globs field uses the project-relative pattern. If your integration code lives in a subdirectory (e.g. apps/api/src/), the patterns still match — Cursor walks the whole tree.