Functions SDK
Full reference of the SDK available in Tenklo Functions.
Examples
import { createSync } from 'nango';
const githubIssueDemoSchema = z.object({ ... });
type GithubIssueDemo = z.infer<typeof githubIssueDemoSchema>;
export default createSync({
description: `Fetches the Github issues from all a user's repositories.`,
version: '1.0.0',
frequency: 'every hour',
autoStart: true,
metadata: z.void(),
models: {
GithubIssueDemo: githubIssueDemoSchema
},
exec: async (nango) => {
// Mark the start of deletion tracking
await nango.trackDeletesStart('GithubIssueDemo');
// Fetch issues from GitHub.
const res = await nango.get({
endpoint: '/repos/NangoHQ/interactive-demo/issues?labels=demo&sort=created&direction=asc'
});
// Map issues to your preferred schema.
const issues: GithubIssueDemo[] = res.data.map(({ id, title, html_url }: any) => {
return { id, title, url: html_url };
});
// Persist issues to the Nango cache.
await nango.batchSave(issues, 'GithubIssueDemo');
// Detect and mark deleted records
await nango.trackDeletesEnd('GithubIssueDemo');
},
});Read more about integration functions to understand what role they play in Tenklo.
Configuration
createSync
descriptionstringrequiredThe description of the sync.
execfunctionrequiredThe function that will be called when the sync is triggered.
frequencystringrequiredThe frequency of the sync.
frequency: 'every 1 minute',
frequency: 'every hour',
frequency: 'every 2 days'
frequency: 'every 3 week'modelsobjectrequiredThe models that will be synced by this function. You need one endpoint per model.
models: {
GithubIssue: z.object({
id: z.string(),
}),
},checkpointZodSchemaOptional schema defining the shape of checkpoint data for this sync. Use checkpoints to save progress and avoid re-fetching all data on every run. See the checkpoints guide for details.
checkpoint: z.object({
cursor: z.string(),
}),autoStartbooleanIf true, automatically runs the sync when a new connection is created.
Otherwise, it needs to be triggered via the API or Tenklo UI.
metadataobjectdefault: z.void()The connection's metadata of the action.
metadata: z.object({
userId: z.string(),
});onWebhookfunctionThe function that will be called when a webhook is received.
scopesstring[]The integration's scopes required by the action. This field is for documentation purposes only and currently not enforced by Tenklo.
scopes: ['read:user', 'write:user'],trackDeletesbooleandeprecated[DEPRECATED] Instead use await nango.trackDeletesStart('modelName') and await nango.trackDeletesEnd('modelName') inside the sync exec function.
When trackDeletes is set to true, Tenklo automatically detects deleted records during full syncs only and marks them as deleted in each record’s metadata (soft delete). These records remain stored in the cache.
When set to false, Tenklo does not mark missing records as deleted, even if they weren’t returned in the latest full sync—they simply remain in the cache unchanged.
Defaults to false.
versionstringThe version of the sync. Use it to track changes to the sync inside Tenklo's UI.
webhookSubscriptionsstring[]default: undefinedThe webhook subscriptions of the sync.
Specify the types of webhooks the method onWebhook will handle.
If a webhook type is not on the list, it will not be handled.
webhookSubscriptions: ['*'],endpointsobject[]deprecated[DEPRECATED] Use the GET /records API endpoint to fetch records.
createAction
descriptionstringrequiredThe description of the sync.
execfunctionrequiredThe function that will be called when the action is triggered.
inputobjectrequiredThe input required by the action when triggering it.
input: z.object({
title: z.string(),
});outputobjectrequiredThe output of the action.
output: z.object({
issueId: z.string(),
});versionstringThe version of the sync. Use it to track changes to the sync inside Tenklo's UI.
metadataobjectdefault: z.void()The connection's metadata of the action.
metadata: z.object({
userId: z.string(),
});scopesstring[]The integration's scopes required by the action. This field is for documentation purposes only and currently not enforced by Tenklo.
scopes: ['read:user', 'write:user'],endpointobjectdeprecated[DEPRECATED] Use the POST /action/trigger API endpoint to trigger actions.
createOnEvent
descriptionstringrequiredThe description of the sync.
event'post-connection-creation' | 'pre-connection-deletion' | 'validate-connection'requiredThe event that will trigger this function.
execfunctionrequiredThe function that will be called when the action is triggered.
versionstringThe version of the onEvent function. Use it to track changes to the onEvent function inside Tenklo's UI.
metadataobjectdefault: z.void()The connection's metadata of the function.
metadata: z.object({
userId: z.string(),
});HTTP requests
Makes an HTTP request inside an integration function:
import type { ProxyConfiguration } from 'nango';
const config: ProxyConfiguration = { endpoint: '/some-endpoint' };
await nango.get(config); // GET request
await nango.post(config); // POST request
await nango.put(config); // PUT request
await nango.patch(config); // PATCH request
await nango.delete(config); // DELETE requestNote that all HTTP requests benefit from automatic credential injection. Because functions are executed in the context of a specific integration & connection, Tenklo can automatically retrieve & refresh the relevant API credentials.
Parameters
Response
HTTP request retries
Configure behavior with two optional fields on your HTTP request config: retries and retryOn.
When a failed request is retried
Tenklo retries only when the failure looks transient. Exact rules depend on the integration:
| Field | Condition for Retry |
|---|---|
| Network Error | If code is one of:ECONNRESET, ETIMEDOUT, ECONNABORTED, ECONNREFUSED, EHOSTUNREACH, EAI_AGAIN |
| Transient HTTP Error | If provider specific proxy.retry.error_code is configured in providers.yaml, only matching statuses are retried.Otherwise, by default, status codes 401, 429, 5xx are retried. Additional status specified in retryOn are always retried in addition to the above statuses. |
If nothing in the table applies, the error is not retried.
Status 401 is retryable so Tenklo can recover after refreshing the connection with potentially updated credentials. If a 401 happens and the connection credentials do not change on the next fetch, Tenklo treats that as a definitive authentication failure and stops performing additional retries
Backoff
Between retry attempts Tenklo waits with exponential backoff: first wait 3000ms, then each wait doubles, capped at 10 minutes.
Uncontrolled fetch
nango.uncontrolledFetch() sends a request without injecting the connection's credentials and without retries. Use it for a host that is not the current integration.
const response = await nango.uncontrolledFetch({
url: new URL('https://sandbox.example.com/upload'),
method: 'POST',
headers: { 'content-type': 'application/octet-stream' },
body: fileContents,
redirect: 'error'
});Parameters
The response is a Fetch Response.
Logging
You can collect logs in integration functions. This is particularly useful when:
- developing, to debug your integration functions
- in production, to collect information about integration function executions & understand issues
Collect logs in integration functions as follows:
await nango.log("This is a log."); // default log level is 'info'
await nango.log("This is an info log", { level: 'info' });
await nango.log("This is a warning", { level: 'warn' });
await nango.log("This is an error", { level: 'error' });Log Levels
Tenklo supports the following log levels (from most to least verbose): debug, info, warn, error, off.
Only logs with a level greater than or equal to the configured logger level will surfaced in the Tenklo UI Logs tab. For example, if the logger level is set to warn, only warn and error logs will be shown.
Default log levels:
- All cloud environments:
warn(only warnings and errors are logged by default) - CLI dry-run:
debug(all logs are shown in the console)
Configuring Log Level
You can override the default log level in two ways:
-
Environment variable: Set
NANGO_LOGGER_LEVELin your environment settings (values:debug,info,warn,error,off). It will apply to all your functions running in this environment and can be overridden in functions by usingnango.setLogger(...) -
In your function code:
nango.setLogger({ level: 'debug' }); // All logs will show in your cloud environments. It will apply to all `nango.log()` following this statement.
nango.setLogger({ level: 'off' }); // No logs will showLogs can be viewed & searched in the Tenklo UI. We plan to make them exportable in the future as well.
You can monitor your usage of custom logs in the billing tab of your Tenklo dashboard.
Environment variables
Integration functions sometimes need to access sensitive variables that should not be revealed directly in the code.
For this, you can define environment variables in the Tenklo UI, in the Environment Settings tab. Then you can retrieve these environment variables from integration functions with:
await nango.getEnvironmentVariables();Parameters
No parameters.
Response
Trigger action
You can call action functions from other integration functions with:
await nango.triggerAction('<ACTION-NAME>', { 'custom_key1': 'custom_value1' });Parameters
Response
Paginate through API responses
Tenklo provides a helper to automatically paginate endpoint responses.
Similar to HTTP requests, the nango.paginate() method takes in a ProxyConfiguration parameter.
Use the paginate field to of the ProxyConfiguration to specify how the endpoint's pagination work. Here's an example for a Jira endpoint:
const config: ProxyConfiguration = {
// https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-search-get
endpoint: `/ex/jira/${cloud.cloudId}/rest/api/3/project/search`,
params: {
properties: properties
},
paginate: {
type: 'offset',
offset_name_in_request: 'startAt',
response_path: 'values',
limit_name_in_request: 'maxResults',
limit: 50,
on_page: async ({ nextPageParam, response }) => {
await nango.log(`Next offset value = ${nextPageParam}`);
await nango.log(`Fetched ${response.data.total} records`);
},
},
headers: {
'X-Atlassian-Token': 'no-check'
},
retries: 10
};
for await (const projects of nango.paginate<JiraProjectResponse>(config)) {
const projectsToSave = toProjects(projects, cloud.baseUrl);
await nango.batchSave(projectsToSave, 'Project');
}As shown in the example above, use a for loop to iterate through the paginated results.
Tenklo has pre-configured the pagination settings for some popular APIs, so you don't have to specify them in functions.
You can view the pre-configured pagination settings for all APIs in the providers.yaml file.
Please note that some APIs have diverging pagination strategies per endpoint, so you might still need to override pre-configured pagination settings at times.
The pagination helper supports 3 types of pagination: cursor, link or offset with the following settings:
Get Integration
Returns the current integration information
await nango.getIntegration();With credentials
await nango.getIntegration({ include: ['credentials'] });Parameters
See GET /integrations/{uniqueKey} query parameters: documentation
Response
See GET /integrations/{uniqueKey} response: documentation
Manage connection metadata
Get connection metadata
Returns the connection's metadata.
await nango.getMetadata();Better, you can specify the type of the metadata;
interface CustomMetadata {
anyKey: Record<string, string>;
}
const myTypedMetadata = await nango.getMetadata<CustomMetadata>();Parameters
No parameters.
Example Response
Set connection metadata
Set custom metadata for the connection (overrides existing metadata).
await nango.setMetadata({ 'CUSTOM_KEY1': 'CUSTOM_VALUE1' });Parameters
Response
Empty response.
Edit connection metadata
Edit custom metadata for the connection. Only overrides & adds specified properties, not the entire metadata.
await nango.updateMetadata({ 'CUSTOM_KEY1': 'CUSTOM_VALUE1' });Parameters
Response
Empty response.
Get the connection credentials
Returns a specific connection with credentials.
await nango.getConnection();The response content depends on the API authentication type (OAuth 2, OAuth 1, API key, Basic auth, etc.).
When you fetch the connection with this API endpoint, Tenklo will check if the access token has expired. If it has, it will refresh it.
We recommend not caching tokens for longer than 5 minutes to ensure they are fresh.
Parameters
Example Response
Sync-specific helper methods
Sync functions persist data updates to the Tenklo cache, which your app later fetches. See the sync functions section.
Checkpoints
Checkpoints allow syncs to save their progress and resume from where they left off. This is useful for:
- Resuming after failures without re-fetching all data
- Tracking pagination state across sync runs
- Storing custom cursor/offset values
To use checkpoints, define a checkpoint schema in your sync configuration (see Configuration). For a complete guide on implementing incremental syncs with checkpoints, see the checkpoints guide. If you are migrating from nango.lastSyncDate, see the migration guide.
getCheckpoint()
Retrieves the current checkpoint. Returns null on first run or after a reset.
const checkpoint = await nango.getCheckpoint();
if (checkpoint) {
// Resume from checkpoint
startCursor = checkpoint.cursor;
}Response
Returns the checkpoint object matching your schema, or null if no checkpoint exists.
saveCheckpoint()
Saves the current progress. Call this after processing each batch of data to enable resumption if the sync fails.
await nango.saveCheckpoint({ cursor: nextCursor });Parameters
clearCheckpoint()
Clears the checkpoint. Use at the end of a full sync so the next run starts from the first page. Checkpoints are also automatically cleared when triggering a sync with reset: true.
await nango.clearCheckpoint();Save records
Upserts records to the Tenklo cache (i.e. create new records, update existing ones). Each record needs to contain a unique id field used to dedupe records.
const githubIssues: GitHubIssue[] = ...; // Fetch issues from GitHub API.
await nango.batchSave(githubIssues, 'GitHubIssue');Parameters
Delete records
Marks records as deleted in the Tenklo cache. Deleted records are still returned when you fetch them, but they are marked as deleted in the record's metadata (i.e. soft delete).
This does not remove cached payloads.
nango.batchDelete() is used to mark records as deleted in Tenklo because they were deleted on the external API. Tenklo may still keep the last-known payload so your customer can react to the deletion event.
If you want to permanently remove data from Tenklo storage for cost or compliance reasons, use record pruning instead.
To implement deletion detection in your syncs, follow this guide.
The only field that needs to be present in each record when calling batchDelete is the unique id; the other fields are ignored.
const githubIssuesToDelete: { id: string }[] = ...; // Fetch issues to delete from GitHub API.
await nango.batchDelete(githubIssuesToDelete, 'GitHubIssue');Parameters
Detect deletions automatically
Automatically detects and marks records as deleted by comparing what existed before trackDeletesStart with what was saved between trackDeletesStart and trackDeletesEnd.
This does not remove cached payloads.
nango.trackDeletesStart()/nango.trackDeletesEnd() are used to mark records as deleted in Tenklo because they were not returned by the external API. Tenklo may still keep the last-known payload so your customer can react to the deletion event.
If you want to permanently remove data from Tenklo storage for cost or compliance reasons, use record pruning instead.
await nango.trackDeletesStart('ModelName');
// ... fetch and save all records ...
await nango.trackDeletesEnd('ModelName');Call trackDeletesStart at the beginning of your sync execution, before fetching any data. Call trackDeletesEnd after all records have been saved with batchSave. Tenklo will compare the records that existed before trackDeletesStart with those saved in the window and mark any missing records as deleted.
Parameters (both functions)
Important considerations:
- Only use within syncs that fetch the complete dataset between
trackDeletesStartandtrackDeletesEnd - If your sync fails or doesn't fetch the complete dataset, avoid calling
trackDeletesEndas it may cause false deletions - Records are soft deleted (marked with
_metadata.deleted = true) and remain in the cache
deleteRecordsFromPreviousExecutions is deprecated. Use trackDeletesStart/trackDeletesEnd instead.
For more details on deletion detection strategies, see the detecting deletes guide.
Update records
Updates records in the Tenklo cache by merging the given data into the existing record. The id field is required in each record and used to determine what existing record to merge into.
batchUpdate is primarily useful in webhook sync functions, where you receive partial updates from a webhook and want to merge them into the existing records.
The merge algorithm used is a deep merge. Nested objects are merged recursively, while arrays always use the new value for the array. Any fields not present in the update record are left unchanged.
// Create partial GitHub Issue update records with only id and state.
const githubIssues: Pick<GitHubIssue, "id" | "state">[] = ...;
await nango.batchUpdate(githubIssues, 'GitHubIssue');Take special care when using batchUpdate with records containing arrays. The merge algorithm does not attempt to merge arrays, but rather always uses the value of the new array.
// given a an existing record:
// { id: '1', tags: [{id: 12, name: 'Dev'}, {id: 13, name: "QA"}] }
const updates: Pick<Issue, "id" | "tags">[] = [
{ id: '1', tags: [{id: 14, name: 'UX'}] }
];
// after the update, the record will be:
// { id: '1', tags: [{id: 14, name: "UX"}] }
await nango.batchUpdate(updates, 'Issue');Parameters
Get records
Fetches records from the Tenklo cache by ID. Returns a Map where the keys are the requested IDs, and the values are the corresponding records. Any records that are not found will simply be absent from the map.
Example usage:
const records = await nango.getRecordsById<string, Issue>(['1', '2', '3'], 'Issue');
if (records.has('1')) {
const record = records.get('1');
await nango.log(record.title);
} else {
await nango.log('Record with id 1 not found.');
}Fetching records by ID is useful when you need to update specific records with a more granular approach than nango.batchUpdate(), which performs a deep merge. Note that nango.batchUpdate() is more performant than using nango.getRecordsById(), followed by nango.batchSave().
A common use case is when handling external webhooks, where only a partial update of a record is received from an API.
Variant
If you are using sync variants, you can access the current variant name via the nango.variant property.
export default createSync({
exec: async (nango) => {
await nango.log(`Running sync with variant: ${nango.variant}`);
// Customize sync behavior based on variant
const res = await nango.get({
endpoint: `/spreadsheet/${nango.variant}`
});
// Rest of sync implementation...
},
});Action-specific helper methods
ActionError
You can use ActionError in an action function to return a descriptive error to your app when needed:
export default createAction({
exec: async (nango) => {
// Something went wrong...
throw new ActionError({ any_key: 'any_value' });
},
});In this case, the response to the trigger action call will be:
{
"error_type": "action_script_failure",
"payload": {
"any_key": "any_value"
}
}Relative imports in functions
You can import relative files into your functions to allow for code abstraction and to
maintain DRY (Don't Repeat Yourself) principles. This means you can reuse code across
different functions by importing it. The imported file must live in the nango-integrations
directory and can be imported in the following way:
import { issueMapper } from '../mappers/issue-mappper';
export default createSync({
exec: async (nango) => {
// Fetch issues from GitHub.
const res = await nango.get({
endpoint: '/repos/NangoHQ/interactive-demo/issues?labels=demo&sort=created&direction=asc'
});
// Persist issues to the Nango cache.
await nango.batchSave(issueMapper(res.data), 'GithubIssueDemo');
},
});Note that you cannot import third-party modules at this time. Additionally, if there is a compilation error in an imported file, the entry point file will also fail to compile.
Pre-included Dependencies
Some libraries are pre-included for usage in functions:
Please reach out in the community if you would like to request additional ones.
Questions, problems, feedback? Please reach out in the Slack community.