Harvest ExecModule
Overview
HarvestModule connects ValkyrAI workflows to Harvest API v2 for client and time-entry management plus bounded project, expense, and invoice inspection. It exposes thirteen operations through the native map I/O ExecModule ABI and keeps the bearer token and numeric Harvest account ID in a READY IntegrationAccount.
The module fixes the production origin to https://api.harvestapp.com/v2, sends the token only in Authorization: Bearer, sends the account binding only in Harvest-Account-Id, bounds request and response sizes, rejects credential-like filter and payload fields, returns one provider page per execution, retries transient reads only, and sends create, update, and delete operations once. Time-entry deletion requires confirmDestructive: true.
n8n's Harvest integration informed the resource and operation split across clients, projects, time entries, expenses, and invoices. ValkyrAI adds a generated IntegrationAccount boundary, fixed-origin enforcement, explicit destructive confirmation, provider-identity checks, bounded output, write reconciliation, and shared credential redaction.
Usage
- Create a Harvest personal access token or OAuth authorization for the minimum account access required.
- Store the token only in the encrypted
apiKeyorpasswordfield of a ValkyrAIIntegrationAccount, store the numeric Harvest account ID inaccountId, set the account toREADY, and bind it throughExecModuleConfig.authConfig.integrationAccount. - Choose one operation and provide its conditional fields.
- For list operations, pass
nextPageintopageon a later execution whenhasMoreistrue. - Reconcile any ambiguous create, update, or delete in Harvest before manually retrying.
Inputs
| Name | Type | Required | Description | Constraints |
|---|---|---|---|---|
operation | string | Yes | Operation listed below. | Exact allowlist of thirteen values. |
recordId | integer | Conditional | Client, project, or time-entry ID. | Positive integer with at most 19 digits. |
filters | object | Conditional | Resource-specific list filters. | At most 12 allowlisted scalar fields; each value at most 2,048 characters. |
record | object | Conditional | Client or time-entry create/update fields. | Non-empty, at most 16 allowlisted fields and 256 KiB serialized; credential-like fields rejected. |
page | integer | No | One-based provider page. | 1–100,000; default 1. |
perPage | integer | No | Provider page size. | 1–2,000; default 100. |
confirmDestructive | boolean | Conditional | Explicit approval for time-entry deletion. | Must be true for delete_time_entry. |
Client filters support is_active and updated_since. Project filters support is_active, client_id, and updated_since. Time-entry filters support user, client, project, task, external-reference, billed/running/approval state, update time, and inclusive date range. Expense filters support user, client, project, billed state, update time, and date range. Invoice filters support client, project, update time, date range, and invoice state.
Client writes allow name, is_active, address, and currency; creation requires a non-empty name. Time-entry writes allow user_id, project_id, task_id, spent_date, hours, start/end times, notes, and external_reference. Creation requires project_id, task_id, and spent_date.
Outputs
| Name | Type | When present | Description |
|---|---|---|---|
status | string | Always | success or error. |
operation | string | Always | Normalized operation. |
data | object | Single-resource success | Verified Harvest resource. |
id | string | Resource response has an ID | Positive provider identity. |
deletedId | string | Delete success | Confirmed requested time-entry ID. |
items | array | List success | One bounded provider page. |
count | integer | List success | Items returned in this page. |
page | integer | List success | Current one-based page. |
total | integer | Harvest supplies total_entries | Provider-reported total entries. |
hasMore | boolean | List success | Whether another page may exist. |
nextPage | integer | Another page exists | Page for the next execution. |
httpStatus | integer | Provider call succeeds | Harvest response status. |
attempts | integer | Provider call or error | HTTP attempts consumed. |
error | object | Failure | Redacted code, message, and retryable fields. |
Harvest results can contain employee, customer, billing, invoice, and time-allocation data and are classified confidential. Tokens and provider bodies are never copied into progress logs.
IntegrationAccount Requirements
| Setting | Requirement |
|---|---|
| Provider | Harvest API v2 |
| Authentication | OAuth2 or personal access token in Authorization: Bearer |
| Status | Exactly READY |
accountName | Human-readable Harvest account identity |
accountId | Numeric Harvest account ID used in Harvest-Account-Id |
apiKey or password | Encrypted bearer token, 20–8,192 non-whitespace characters |
| Relationship | ExecModuleConfig.authConfig.integrationAccount |
| Suggested access | Only the selected Harvest account and the least administrative permission needed |
Harvest personal access tokens can access authorized accounts broadly. Prefer OAuth with the smallest account scope when multiple users will run workflows, and rotate/revoke tokens independently from workflow definitions.
Configuration
Illustrative persisted configuration:
{
"version": "1.0.0",
"authConfig": {
"authStrategy": 1,
"integrationAccount": "integration-account:harvest-revenue-ops"
},
"payloadConfig": {
"parameters": "{\"operation\":\"list_time_entries\",\"page\":1,\"perPage\":100,\"filters\":{\"project_id\":14308069,\"is_billed\":false}}"
}
}
The relationship is symbolic. The generated IntegrationAccount record owns encrypted credentials and the account binding.
Operations
| Operation | Harvest path | Behavior |
|---|---|---|
get_current_user | GET /users/me | Read the authenticated Harvest user. |
list_clients | GET /clients | List clients with bounded active/update filters and pagination. |
get_client | GET /clients/{clientId} | Read one client and verify its ID. |
create_client | POST /clients | Create one client; single-attempt write. |
list_projects | GET /projects | List projects by client, active state, or update time. |
get_project | GET /projects/{projectId} | Read one project and verify its ID. |
list_time_entries | GET /time_entries | List bounded time entries with documented filters. |
get_time_entry | GET /time_entries/{timeEntryId} | Read one time entry and verify its ID. |
create_time_entry | POST /time_entries | Create one time entry; single-attempt write. |
update_time_entry | PATCH /time_entries/{timeEntryId} | Update one entry and require the same returned ID. |
delete_time_entry | DELETE /time_entries/{timeEntryId} | Permanently delete one eligible entry after explicit confirmation. |
list_expenses | GET /expenses | Inspect expenses with account-safe filters and pagination. |
list_invoices | GET /invoices | Inspect invoices by client/project/date/state. |
Project/client update or deletion, expense mutations/receipt transfer, invoice creation/sending/payment, estimates, assignments, approval submission, timer start/stop shortcuts, and reports are deferred. These surfaces introduce separate attachment, outbound-message, billing, approval, or destructive contracts.
Errors and Failure Modes
| Failure | Cause | Retry guidance |
|---|---|---|
VALIDATION_ERROR | Missing field, invalid ID/filter/bound, absent delete confirmation, unsafe write key, invalid credential binding, or mismatched provider identity. | Correct the request; local validation sends no provider call. |
INTEGRATION_ACCOUNT_REQUIRED | No bound secure account. | Bind the intended Harvest account. |
INTEGRATION_ACCOUNT_NOT_READY | Account status is not READY. | Repair or reauthorize the account. |
HARVEST_HTTP_401 | Token is missing, expired, or invalid. | Reauthorize or rotate the token. |
HARVEST_HTTP_403 | User or token lacks permission for the account/resource. | Grant only the required account permission. |
HARVEST_HTTP_404 | Resource is absent or not visible to this account. | Reconcile the account and resource ID. |
HARVEST_HTTP_422 | Provider rejected a field, state transition, locked entry, or permission-dependent mutation. | Correct the request or provider state; do not blindly retry. |
HARVEST_HTTP_429 | API rate limit exceeded. | Reads use bounded retry and numeric Retry-After; writes remain single-attempt. |
HARVEST_HTTP_5xx / NETWORK_ERROR | Provider or transport failure. | Safe reads retry up to three total attempts. Reconcile writes before manual retry. |
RESPONSE_TOO_LARGE | Provider response exceeded 5 MiB. | Narrow filters or page size. |
Provider messages are bounded to 500 characters and pass through token replacement plus the shared sensitive-data policy. A failed write may have reached Harvest, so the module refuses automatic replay.
Example
Find unbilled August time for one project:
{
"operation": "list_time_entries",
"page": 1,
"perPage": 100,
"filters": {
"project_id": 14308069,
"is_billed": false,
"from": "2026-08-01",
"to": "2026-08-31"
}
}
Expected normalized result:
{
"status": "success",
"operation": "list_time_entries",
"items": [
{
"id": 636708723,
"spent_date": "2026-08-12",
"hours": 2.5,
"project": {"id": 14308069, "name": "Enterprise AI"}
}
],
"count": 1,
"page": 1,
"total": 1,
"hasMore": false,
"httpStatus": 200,
"attempts": 1
}
Notes
- Pagination: one execution returns one provider page. Reuse
nextPageaspage; no silent all-pages loop occurs. Harvest permitsper_pagefrom 1 through 2,000, while this module also caps page at 100,000 and response bytes at 5 MiB. - Rate limits:
429,408, and selected5xxstatuses are transient only for reads. The module honors a numericRetry-Afterup to five seconds and caps reads at three attempts. - API limits: request JSON is capped at 256 KiB, list filters at 12 scalar values, mutable records at 16 fields, and filter values at 2,048 characters. Harvest permissions still determine which users, entries, projects, expenses, and invoices are visible.
- Idempotency: reads are transport-retry-safe. Client and time-entry creation, time-entry update, and deletion are single-attempt and may have committed before a network failure. Reconcile by resource ID, project/task/date, or client name before retrying.
- Destructive behavior:
delete_time_entrypermanently removes an eligible entry and requiresconfirmDestructive: true. Locked/approved/invoiced state and project/task archival can restrict deletion. No bulk deletion is exposed. - Billing and privacy: time, expense, invoice, client, and employee data is confidential. Apply least privilege and human review before using output for payroll, billing, performance evaluation, or external communication.
- External verification: deterministic tests cover validation, request construction, bearer/account headers, pagination, create/update/delete behavior, destructive confirmation, transient read retries, write non-retry, redaction, identity checks, fixed origin, response normalization, and metadata discovery. Live provider execution is deferred until a separately authorized Harvest account and token are supplied.
See the official Harvest API v2 overview, authentication contract, pagination guidance, time-entry API, and invoice API for provider behavior. The official n8n Harvest node's client, project, time-entry, expense, invoice, user, task, and contact resources informed the functional reference.