> ## 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

> Drop-in .cursor/rules/ files that make Cursor's code generation VH3-aware, correct field names, proper error handling, n8n workflow conventions

# Cursor Rules

Cursor reads `.cursor/rules/*.mdc` files automatically and applies them to chats based on the rule's scope. Unlike [AGENTS.md](/agent-kits/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.

<Note>
  **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.
</Note>

## File 1: `vh3-api-domain.mdc` (always apply)

This rule gives Cursor persistent VH3 vocabulary. It applies to every chat in the project.

<CodeGroup>
  ````markdown .cursor/rules/vh3-api-domain.mdc theme={null}
  ---
  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.
  ````
</CodeGroup>

## File 2: `vh3-api-calls.mdc` (TypeScript / JavaScript / Python)

This rule applies when writing API integration code in `.ts`, `.js`, or `.py` files.

<CodeGroup>
  ````markdown .cursor/rules/vh3-api-calls.mdc theme={null}
  ---
  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.
  ````
</CodeGroup>

## File 3: `vh3-n8n-workflows.mdc` (n8n workflows)

For projects using [n8n-as-code](/n8n-node) to build VH3 workflows.

<CodeGroup>
  ````markdown .cursor/rules/vh3-n8n-workflows.mdc theme={null}
  ---
  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, connect the **VH3 AI Tool** sub-node from the
  community node (`n8n-nodes-vh3ai`). Tool names, parameters, and routing
  hints live on the tool definitions; do not duplicate them in the system prompt.

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

  ## 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
  ````
</CodeGroup>

## Installing the rules

<Steps>
  <Step title="Create the rules directory">
    ```bash theme={null}
    mkdir -p .cursor/rules
    ```
  </Step>

  <Step title="Drop the three files in">
    Copy each block above into the corresponding filename inside `.cursor/rules/`.
  </Step>

  <Step title="Reload Cursor">
    Cursor reads `.mdc` files on project load. Reload the window or reopen the project to pick up changes.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## How the rules interact

<CardGroup cols={2}>
  <Card title="Domain rule (always)" icon="book">
    `vh3-api-domain.mdc` applies to every chat. Cursor always knows the entity model and field names.
  </Card>

  <Card title="Code rule (.ts/.py)" icon="code">
    `vh3-api-calls.mdc` applies only when editing implementation files. Shapes generated code.
  </Card>

  <Card title="n8n rule (.workflow.ts)" icon="webhook">
    `vh3-n8n-workflows.mdc` applies inside n8n-as-code projects. Enforces credential and timeout conventions.
  </Card>

  <Card title="AGENTS.md (conversation)" icon="message">
    AGENTS.md handles routing and reasoning. Cursor rules handle code-level correctness. Use both.
  </Card>
</CardGroup>

<Warning>
  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.
</Warning>
