Google Contacts ExecModule
Overview
GoogleContactsModule connects ValkyrAI workflows to Google People API v1. It uses the native map I/O ExecModule ABI, appears in the annotation-backed Workflow Studio catalog as GoogleContactsModule, and resolves OAuth credentials only from an IntegrationAccount.
The connector implements the highest-value contact behavior from n8n's Google Contacts node and adds bounded contact-group discovery:
list_contactssearch_contactsget_contactcreate_contactupdate_contactdelete_contactlist_contact_groupsget_contact_group
The Google People API origin and OAuth token cannot be overridden by workflow input.
Usage
- Enable Google People API in the Google Cloud project that owns the OAuth client.
- Authorize the minimum Contacts OAuth scope required by the workflow.
- Store the current OAuth access token in a Google
IntegrationAccountSecureField and keep the account inREADYstatus. - Add
GoogleContactsModuleto a workflow and bind that account throughExecModuleConfig.authConfig.integrationAccount. - Set
operationand the operation-specific fields in module configuration or map them from upstream workflow state.
Mapped workflow input can override non-secret contact fields. Authentication, the Google API host, field allowlist, retry safety, response limits, etag enforcement, and destructive confirmation remain trusted module behavior.
Inputs
| Name | Type | Required for | Default | Constraints |
|---|---|---|---|---|
operation | string | Every execution | None | One of the eight documented operations. |
contactId | string | get_contact, update_contact, delete_contact | None | Bare Google person ID or people/<id>; 1–256 ID characters. |
contactGroupId | string | get_contact_group | None | Bare group ID or contactGroups/<id>. |
query | string | search_contacts | None | 1–256 characters. |
personFields | string | Read/list/search operations | names,emailAddresses,phoneNumbers,organizations,memberships | Comma-separated values from the module's People API field allowlist. |
givenName / familyName | string | At least one for create_contact; optional for update | None | Each supplied value is at most 256 characters. Empty update values clear names when both are empty. |
email | string | Optional create/update field | None | Valid address, at most 320 characters; an empty update clears email addresses. |
emailType | string | When email is non-empty | work | home, work, or other. |
phone | string | Optional create/update field | None | 3–64 phone characters; an empty update clears phone numbers. |
phoneType | string | When phone is non-empty | mobile | home, work, mobile, main, or other. |
organizationName / jobTitle | string | Optional create/update fields | None | Each supplied value is at most 512 characters; empty values can clear organization data. |
notes | string | Optional create/update field | None | Plain text up to 8,192 characters; an empty update clears biographies. |
etag | string | update_contact | None | Required optimistic-concurrency token from a prior read. |
pageToken | string | Optional list resume point | None | Opaque; maximum 4,096 characters. |
limit | integer | Optional list/group bound | 100 | 1–10,000; contact searches use provider pages of at most 30. |
returnAll | boolean | Optional list behavior | false | Continues pagination, safety-capped at 10,000 items. |
warmupCache | boolean | Optional search_contacts behavior | true | Performs the Google-recommended empty-query cache warmup before search. |
confirmDelete | boolean | delete_contact | false | Must be true because deletion is permanent. |
Outputs
Every execution returns stable status, operation, and attempts fields.
| Name | Type | When present | Description |
|---|---|---|---|
status | string | Always | success or error. |
operation | string | Always | Normalized operation name. |
attempts | integer | Always | Provider HTTP attempts. |
httpStatus | integer | Provider response received | Last Google People HTTP status. |
data | object | Single-resource or mutation success | Normalized Google person/group response; empty for HTTP 204. |
resourceName | string | Provider resource contains one | Canonical people/<id> or contactGroups/<id> name. |
contactId | string | Person resource contains resourceName | Convenience person ID without the people/ prefix. |
etag | string | Provider resource contains one | Concurrency token for a later update. |
items | array | List/search success | Bounded contacts or contact groups. Search result wrappers are normalized to person objects. |
count | integer | List/search success | Number of returned items. |
hasMore | boolean | List/search success | Whether Google returned another page token. |
nextPageToken | string | Another page exists | Opaque token for a later execution. |
requestId | string | Google supplies one | Provider request reference for support. |
error | object | Failure | Safe {code, message, httpStatus?, retryable} details. |
OAuth tokens are never returned. Provider errors are bounded and redacted through the shared workflow sensitive-data policy.
IntegrationAccount Requirements
| Setting | Requirement |
|---|---|
| Provider | Google Contacts / Google Workspace |
| Auth mechanism | OAuth 2.0 bearer access token |
accountName | Human-readable Google account or Workspace label |
apiKey | Preferred encrypted SecureField containing the access token |
password | Legacy token fallback only; prefer apiKey |
status | Must be READY; all other states fail closed |
Use the minimum applicable scope:
- Read contact and group data:
https://www.googleapis.com/auth/contacts.readonly - Create, update, or delete contacts:
https://www.googleapis.com/auth/contacts
See Google People API authorization. Never place tokens in parameters, mapped workflow input, examples, logs, or outputs. Use the platform OAuth refresh flow to rotate expired access tokens.
Configuration
Module-specific fields belong in ExecModulePayloadConfig.parameters; retries and timeouts use existing normalized configuration groups.
{
"version": "1.0.0",
"authConfig": {
"authStrategy": 1,
"integrationAccount": "integration-account:google-contacts-primary"
},
"retryPolicy": {
"maxAttempts": 3,
"backoffStrategy": "EXPONENTIAL",
"initialDelayMs": 1000,
"maxDelayMs": 60000,
"jitter": false
},
"executionConfig": {
"timeoutMs": 30000
},
"payloadConfig": {
"parameters": "{\"operation\":\"search_contacts\",\"query\":\"avery@example.com\",\"limit\":30}"
}
}
The integrationAccount value is illustrative. Persisted workflows use the generated relationship, never a plaintext token.
Operations
| Operation | Provider behavior | Side effect and retry behavior |
|---|---|---|
list_contacts | Lists the authorized principal's contacts, sorted by recent modification. | Read-only; retries transient failures and paginates. |
search_contacts | Warms the search cache, searches contacts, and unwraps result persons. | Read-only; retries transient failures; each provider page is at most 30. |
get_contact | Reads one person with allowlisted fields. | Read-only; retries transient failures. |
create_contact | Creates a person with typed name, email, phone, organization, title, and notes. | Write; single attempt because an ambiguous response may have committed. |
update_contact | Patches only supplied field families and requires the current etag. | Write; single attempt; empty supplied fields can clear provider arrays. |
delete_contact | Permanently deletes one contact. | Irreversible; requires confirmDelete=true; single attempt. |
list_contact_groups | Lists visible contact groups with name, type, and member count. | Read-only; retries transient failures and paginates. |
get_contact_group | Reads one group and a bounded member list. | Read-only; retries transient failures. |
Errors and Failure Modes
| Code | Typical cause | Retryable | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Missing/invalid ID, search text, field mask, email, phone, etag, mutable field, or confirmation. | No | Correct inputs; no unsafe request was sent. |
UNSUPPORTED_OPERATION | Unknown operation name. | No | Select a documented operation. |
INTEGRATION_ACCOUNT_REQUIRED | No account is bound. | No | Bind a Google Contacts IntegrationAccount. |
INTEGRATION_ACCOUNT_NOT_READY | Account status is not READY. | No | Reconnect or repair the account. |
GOOGLE_CONTACTS_HTTP_401 | OAuth token is expired or invalid. | No | Refresh the token. |
GOOGLE_CONTACTS_HTTP_403 | Scope/resource access is missing, or a quota was exceeded. | Depends on reason | Grant scope/access; rate-limit reasons retry only for reads. |
GOOGLE_CONTACTS_HTTP_404 | Person or group ID is wrong or invisible. | No | Verify the resource ID and authorized principal. |
GOOGLE_CONTACTS_HTTP_409 | The supplied etag is stale or the provider detected a write conflict. | No | Read the contact again, reconcile, and intentionally retry. |
GOOGLE_CONTACTS_HTTP_429 | Request quota exceeded. | Yes for reads | Honor provider guidance and retry later. |
RESPONSE_TOO_LARGE | Response exceeded 5 MiB. | No | Narrow fields/query or lower limit. |
NETWORK_ERROR | DNS, TLS, timeout, or connectivity failure. | Yes for reads | Verify connectivity and retry. |
INVALID_PROVIDER_RESPONSE | Google returned a non-list collection field. | No | Capture request ID and check API compatibility. |
Example
Create an approved deal contact:
{
"operation": "create_contact",
"givenName": "Avery",
"familyName": "Chen",
"email": "avery@example.com",
"emailType": "work",
"phone": "+1 415 555 0100",
"phoneType": "mobile",
"organizationName": "Example Co",
"jobTitle": "VP Operations",
"notes": "Approved opportunity contact; follow up after the discovery call."
}
Expected result shape:
{
"status": "success",
"operation": "create_contact",
"attempts": 1,
"httpStatus": 200,
"resourceName": "people/c123_contact",
"contactId": "c123_contact",
"etag": "provider-etag",
"data": {
"resourceName": "people/c123_contact",
"contactId": "c123_contact",
"etag": "provider-etag"
}
}
Notes
- Pagination: contact and group lists request at most 1,000 items per Google page; search requests at most 30. Opaque
nextPageTokenvalues are preserved, andreturnAllis hard-capped at 10,000 items. - Rate limits: read operations retry HTTP 408/429/5xx and Google
rateLimitExceeded,userRateLimitExceeded, orbackendErrorreasons. Integer and RFC 1123Retry-Aftervalues are honored. Writes are never automatically retried. - API limits: the module covers contacts and contact-group discovery, not directory profiles, other contacts, photos, batch mutations, or delegated domain-wide administration.
personFieldsis allowlisted to prevent arbitrary field-mask injection. - Idempotency: create, update, and delete use one provider attempt. After a timeout, search/read by immutable
resourceNameand reconcile before replaying. - Concurrency:
update_contactrequires the current provideretagand sends an explicitupdatePersonFieldsmask. A stale etag must be reconciled instead of overwritten. - Destructive behavior:
delete_contactis permanent and requiresconfirmDelete=true; automatic retries are disabled. - Search cache: Google People search may need a warmup request.
warmupCache=truefollows the behavior used by n8n's Google Contacts node. - External verification: local tests use a deterministic mock provider. Live Google behavior remains unverified until an authorized Google Contacts credential is supplied.
- Functional reference: n8n's current Google Contacts node supports create, delete, get, get-many/search, and update, including People API field masks, pagination, contact groups, etags, and cache warmup. ValkyrAI adds strict field allowlisting, bounded responses, explicit destructive confirmation, and read-only retry boundaries.