External API calls
Comind.work action logic runs on the server in a restricted runtime. The standard browser APIs like fetch() and libraries like axios are not available. Instead, use the RestApi global to call external REST APIs from action logic.
The RestApi builder
RestApi exposes a fluent builder pattern. Every call follows the same four steps:
- Create a client with a base URL.
- Create a request with a path and HTTP method.
- Configure authentication, headers, query parameters, and body.
- Execute the request and process the response.
import { RestApi } from "@comind/api/server";
// 1. Base URL
RestApi.createClient("https://api.example.com");
// 2. Path + method
RestApi.createRequest("/v1/contacts", "GET");
// 3. Headers, auth, body (as needed)
RestApi.addHeader("Accept", "application/json");
// 4. Fire
const res = RestApi.executeRequest();
const data = JSON.parse(res.content);
The response object contains statusCode, content (the body as a string), and other metadata depending on the request type.
Each createClient / createRequest pair configures a single request. Call the pair again for each subsequent request.
Authentication patterns
Basic auth
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/data", "GET");
RestApi.setHttpBasicAuth("username", "password");
const res = RestApi.executeRequest();
Bearer token (OAuth 2)
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/data", "GET");
RestApi.setOAuth2AuthorizationHeaderAuth(token);
const res = RestApi.executeRequest();
API key in a header
Some services expect a custom header instead of standard auth:
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/data", "GET");
RestApi.addHeader("X-API-Key", apiKey);
const res = RestApi.executeRequest();
Storing credentials securely
Never hardcode secrets in action logic. Store them as COMIND_SECRET_* environment variables and retrieve them at runtime with AppSchema.getSecrets().
Define the secrets in your app's settings/secrets.ts:
// settings/secrets.ts
export const secrets = {
crmApiKey: process.env.COMIND_SECRET_CRM_API_KEY,
crmBaseUrl: process.env.COMIND_SECRET_CRM_BASE_URL,
};
Then read them inside action logic:
import { AppSchema, RestApi } from "@comind/api/server";
const { COMIND_SECRET_CRM_API_KEY, COMIND_SECRET_CRM_BASE_URL } =
AppSchema.getSecrets();
if (!COMIND_SECRET_CRM_API_KEY) {
throw "Missing secret: COMIND_SECRET_CRM_API_KEY";
}
RestApi.createClient(COMIND_SECRET_CRM_BASE_URL);
RestApi.createRequest("/contacts", "GET");
RestApi.addHeader("X-API-Key", COMIND_SECRET_CRM_API_KEY);
const res = RestApi.executeRequest();
See Settings model for the full details on variables, secrets, and the settings/ directory structure.
Making requests
GET with query parameters
import { RestApi } from "@comind/api/server";
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/contacts", "GET");
RestApi.addQueryParameter("status", "active");
RestApi.addQueryParameter("limit", "50");
RestApi.addQueryParameter("offset", "0");
const res = RestApi.executeRequest();
if (res.statusCode !== 200) {
throw `GET /contacts failed with status ${res.statusCode}: ${res.content}`;
}
const contacts = JSON.parse(res.content);
console.log(`Fetched ${contacts.length} contacts`);
POST with JSON body
import { entity } from "#typings";
import { RestApi } from "@comind/api/server";
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/contacts", "POST");
RestApi.addHeader("Content-Type", "application/json");
RestApi.addJsonBody({
name: entity.title,
email: entity.c_email,
company: entity.c_company_name,
source: "comind-sync",
});
const res = RestApi.executeRequest();
if (res.statusCode !== 201) {
throw `POST /contacts failed: ${res.content}`;
}
const created = JSON.parse(res.content);
entity.c_external_id = created.id;
console.log(`Created external contact ${created.id}`);
PUT to update an existing resource
RestApi.createClient("https://api.example.com");
RestApi.createRequest(`/contacts/${entity.c_external_id}`, "PUT");
RestApi.addHeader("Content-Type", "application/json");
RestApi.addJsonBody({
name: entity.title,
email: entity.c_email,
updated_at: new Date().toISOString(),
});
const res = RestApi.executeRequest();
if (res.statusCode !== 200) {
throw `PUT /contacts/${entity.c_external_id} failed: ${res.content}`;
}
POST with file upload
import { Files, RestApi } from "@comind/api/server";
const fileRef = Files.getFileRef(entity.attachments__list[0].file_uid);
RestApi.createClient("https://api.example.com");
RestApi.createRequest("/uploads", "POST");
RestApi.addHeader("Authorization", "Bearer " + token);
RestApi.addFile("file", fileRef);
const res = RestApi.executeRequest();
Handling responses
Every call to executeRequest() returns a response object. Always check the status code before processing the body.
const res = RestApi.executeRequest();
// Check for success
if (res.statusCode >= 200 && res.statusCode < 300) {
const data = JSON.parse(res.content);
console.log("Success:", JSON.stringify(data));
} else if (res.statusCode === 401) {
throw "Authentication failed - check your API credentials";
} else if (res.statusCode === 404) {
console.warn("Resource not found, skipping");
} else if (res.statusCode === 429) {
throw "Rate limit exceeded - try again later";
} else {
throw `Request failed with status ${res.statusCode}: ${res.content}`;
}
Use console.log() to write request and response details to the server logs during development. This is the primary debugging tool for server-side action logic.
Downloading files
Use RestApi.downloadFile() instead of executeRequest() when the response is a binary file. The returned object includes a downloadedFileRef that you can persist and attach to a record.
import { entity } from "#typings";
import { Files, RestApi } from "@comind/api/server";
RestApi.createClient("https://reports.example.com");
RestApi.createRequest("/export/monthly-report.pdf", "GET");
RestApi.addHeader("Authorization", "Bearer " + token);
const res = RestApi.downloadFile();
if (res.statusCode !== 200 || !res.downloadedFileRef) {
throw `File download failed: ${res.statusCode}`;
}
// Persist the temporary file reference
const stored = Files.persistFileRef(res.downloadedFileRef.ref);
// Attach to the record
entity.attachments__list ??= [];
entity.attachments__list.push({
file_uid: stored.uploadedFileId,
title: "monthly-report.pdf",
});
console.log(`Downloaded and attached report (${res.contentLength} bytes)`);
For a step-by-step walkthrough of the download-and-attach pattern, see How to download a file by URL and save as attachment.
Complete example: sync contacts with an external CRM
This example fetches contacts from an external CRM API, then creates or updates matching records in Comind.work using ComindServer.importData.
import { entity } from "#typings";
import { AppSchema, ComindServer, RestApi } from "@comind/api/server";
function sync_contacts() {
// 1. Retrieve credentials from secrets
const { COMIND_SECRET_CRM_API_KEY, COMIND_SECRET_CRM_BASE_URL } =
AppSchema.getSecrets();
if (!COMIND_SECRET_CRM_API_KEY || !COMIND_SECRET_CRM_BASE_URL) {
throw "Missing CRM credentials. Configure COMIND_SECRET_CRM_API_KEY and COMIND_SECRET_CRM_BASE_URL in app secrets.";
}
// 2. Fetch contacts from external CRM
RestApi.createClient(COMIND_SECRET_CRM_BASE_URL);
RestApi.createRequest("/api/v2/contacts", "GET");
RestApi.addHeader("X-API-Key", COMIND_SECRET_CRM_API_KEY);
RestApi.addQueryParameter("updated_since", entity.c_last_sync_date || "");
RestApi.addQueryParameter("limit", "200");
RestApi.setRequestTimeout(30);
const res = RestApi.executeRequest();
if (res.statusCode !== 200) {
throw `CRM API returned ${res.statusCode}: ${res.content}`;
}
const crmContacts = JSON.parse(res.content).data;
console.log(`Fetched ${crmContacts.length} contacts from CRM`);
if (crmContacts.length === 0) {
entity.c_last_sync_status = "No new contacts";
entity.c_last_sync_date = new Date().toISOString();
return;
}
// 3. Map external data to Comind.work fields
const mapped = crmContacts.map((c: any) => ({
title: c.full_name,
c_email: c.email,
c_phone: c.phone,
c_company_name: c.company,
c_external_id: String(c.id),
c_source: "crm-sync",
}));
// 4. Import into Comind.work - duplicates matched by c_external_id
const result = ComindServer.importData({
data: mapped,
keyFields: ["c_external_id"],
});
console.log(
`Import complete: ${result.created} created, ${result.updated} updated`
);
// 5. Update sync status on the trigger record
entity.c_last_sync_status = `Synced ${crmContacts.length} contacts`;
entity.c_last_sync_date = new Date().toISOString();
}
export default {
sync_contacts,
} satisfies ActionLogic<typeof entity>;
How the example works
- Credentials are loaded from app secrets - never hardcoded.
- GET request fetches updated contacts with query parameters for incremental sync.
- Timeout is set to 30 seconds with
setRequestTimeout(30)to avoid blocking indefinitely. - Field mapping transforms external field names to the app's field names.
ComindServer.importDatahandles upsert logic - it creates new records and updates existing ones, matching onc_external_id.- Sync metadata is stored on the trigger record for audit purposes.
Tips
- Synchronous execution:
RestApiruns synchronously on the server. Long-running requests block the action until they complete. UsesetRequestTimeout(seconds)to set an upper bound for unreliable APIs. - Secrets only: store all credentials in
settings/secrets.tsusingCOMIND_SECRET_*environment variables. Never hardcode API keys, tokens, or passwords. - Debugging: use
console.log()to inspect requests and responses in the server logs. Temporarily store raw responses in a text field on the record (like the AI action example does withc_json_response). - No browser APIs:
fetch(),XMLHttpRequest, andaxiosare not available in the server runtime.RestApiis the only HTTP client. - Error handling: always check
res.statusCodebefore parsingres.content. Uncaught exceptions in action logic surface as action errors in the UI. - Pagination: if the external API returns paginated results, loop with
createClient/createRequestpairs, adjustingoffsetorpagequery parameters on each iteration. - Rate limits: some APIs enforce rate limits. If you need to make many requests in a loop, consider adding
ComindServer.sleep(1000)between calls to stay within limits.
Related
- How to create an AI-based action - complete example using
RestApito call the OpenAI API from action logic - How to download a file by URL and save as attachment - download binary files and attach them to records