This guide shows how to use the scheduled run endpoint for Extract LinkedIn Messages with incremental sync via Engagement Identities, and how to migrate existing schedules to incremental mode using the /actions/linkedin-extract-messages/run/schedule route.
Incremental mode fetches only new data since the last retrieval instead of the full dataset on every run. It is available only on engagement-free actions and requires an Engagement Identity (type: "engagement").
Prerequisites
- Engagement Identities enabled in your workspace and at least one Engagement Identity created with a connected LinkedIn account
- API key with access to runs, schedules, and identities
Part 1: Implementing Incremental Mode
1.1 Create a new scheduled run in incremental mode
To schedule Extract LinkedIn Messages in incremental mode, send sync_mode: "incremental" in the parameters object and use an Engagement Identity in identity_ids.
Behavior:
- First run: Full sync (capped by
max_results).
- Next runs: Only new messages since the last run. No need to call continue; each scheduled execution is automatically incremental.
In schedule mode, if the previous run is still in progress when the next iteration is due, that iteration is skipped. The following one will run the incremental update.
JavaScript example: create a scheduled incremental Extract Messages run
const EDGES_API_BASE = 'https://api.edges.run/v1';
const ACTION_SCHEDULE = `${EDGES_API_BASE}/actions/linkedin-extract-messages/run/schedule`;
async function createIncrementalExtractMessagesSchedule(apiKey, options) {
const {
identityId,
conversationId,
callbackUrl,
cron = '0 9 * * *', // daily at 9am
timezone = 'Europe/Paris',
maxResults,
} = options;
const body = {
inputs: [{ conversation_id: conversationId }],
callback: { url: callbackUrl },
identity_ids: [identityId],
cron,
timezone,
parameters: {
sync_mode: 'incremental',
...(maxResults != null && { max_results: maxResults }),
},
};
const res = await fetch(ACTION_SCHEDULE, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message ?? `Schedule create failed: ${res.status}`);
}
return res.json();
}
// Usage: ensure identityId is an Engagement Identity (type === 'engagement')
// const schedule = await createIncrementalExtractMessagesSchedule(process.env.EDGES_API_KEY, {
// identityId: 'id_xxx',
// conversationId: 'ACoAAB...',
// callbackUrl: 'https://yourdomain.com/webhooks/edges',
// });
1.2 Async mode: continue an incremental run
If you use async instead of schedule, you trigger each incremental update by calling the continue endpoint after the previous run has finished.
JavaScript example: run async and continue later
const EDGES_API_BASE = 'https://api.edges.run/v1';
const ACTION_ASYNC = `${EDGES_API_BASE}/actions/linkedin-extract-messages/run/async`;
async function runExtractMessagesIncrementalAsync(apiKey, options) {
const res = await fetch(ACTION_ASYNC, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: [{ conversation_id: options.conversationId }],
callback: { url: options.callbackUrl },
identity_ids: [options.identityId],
parameters: {
sync_mode: 'incremental',
...(options.maxResults != null && { max_results: options.maxResults }),
},
}),
});
if (!res.ok) throw new Error(`Async run failed: ${res.status}`);
return res.json();
}
async function continueIncrementalRun(apiKey, runUid) {
const res = await fetch(`${EDGES_API_BASE}/runs/${runUid}/continue`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
});
if (!res.ok) throw new Error(`Continue failed: ${res.status}`);
return res.json();
}
Part 2: Migrating full-mode schedules to incremental
Schedules cannot be updated in place (e.g. to add parameters.sync_mode). To switch existing Extract Messages schedules from full to incremental, you must:
- List schedules for
linkedin-extract-messages (optionally restricted to Engagement Identities).
- For each schedule: get full details, cancel it, then create a new schedule with the same config plus
parameters.sync_mode: "incremental".
Use only Engagement Identities for the new schedules; incremental sync is not supported for standard identities.
Schedule objects in the examples have optional fields such as scheduled_run_uid, uid, identity_ids, inputs, callback, cron, timezone, parameters, max_results.
2.1 Get Engagement Identity IDs
Filter identities with type: "engagement" so you only migrate schedules that use them (or only recreate schedules using those identities).
const EDGES_API_BASE = 'https://api.edges.run/v1';
async function getEngagementIdentityIds(apiKey) {
const res = await fetch(`${EDGES_API_BASE}/identities?limit=100`, {
headers: { 'X-API-Key': apiKey },
});
if (!res.ok) throw new Error(`List identities failed: ${res.status}`);
const data = await res.json();
const items = data.data ?? data.items ?? data ?? [];
return items
.filter((id) => id.type === 'engagement')
.map((id) => id.uid);
}
List schedules for the Extract Messages action. Use query params as supported by your API (e.g. action_name or action_slug).
async function listExtractMessagesSchedules(apiKey) {
const params = new URLSearchParams({
action_name: 'linkedin-extract-messages',
status: 'ACTIVE',
limit: '100',
});
const res = await fetch(`${EDGES_API_BASE}/schedules?${params}`, {
headers: { 'X-API-Key': apiKey },
});
if (!res.ok) throw new Error(`List schedules failed: ${res.status}`);
const data = await res.json();
return data.data ?? data.items ?? data ?? [];
}
2.3 Filter by Engagement Identity (optional)
If the list response includes identity_ids (or similar), keep only schedules whose identities are in your set of Engagement Identity UIDs. If the API does not return identity info, you can still migrate all Extract Messages schedules and rely on the fact that the new schedule you create will use an Engagement Identity.
function filterSchedulesByEngagementIdentities(schedules, engagementIdentityIds) {
const set = new Set(engagementIdentityIds);
return schedules.filter((s) => {
const ids = s.identity_ids ?? [];
return ids.some((id) => set.has(id));
});
}
2.4 Cancel and recreate each schedule with incremental mode
For each schedule to migrate:
- GET the schedule by
scheduled_run_uid to get inputs, callback, cron, timezone, identity_ids, etc.
- POST
.../schedules/{scheduled_run_uid}/cancel to delete it.
- POST
.../actions/linkedin-extract-messages/run/schedule with the same config plus parameters: { sync_mode: 'incremental' }.
Migration helpers:
async function getSchedule(apiKey, scheduledRunUid) {
const res = await fetch(`${EDGES_API_BASE}/schedules/${scheduledRunUid}`, {
headers: { 'X-API-Key': apiKey },
});
if (!res.ok) throw new Error(`Get schedule failed: ${res.status}`);
return res.json();
}
async function cancelSchedule(apiKey, scheduledRunUid) {
const res = await fetch(
`${EDGES_API_BASE}/schedules/${scheduledRunUid}/cancel`,
{
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
}
);
if (!res.ok) throw new Error(`Cancel schedule failed: ${res.status}`);
}
async function migrateScheduleToIncremental(apiKey, scheduledRunUid) {
const schedule = await getSchedule(apiKey, scheduledRunUid);
await cancelSchedule(apiKey, scheduledRunUid);
const body = {
inputs: schedule.inputs ?? [],
callback: schedule.callback ?? {},
identity_ids: schedule.identity_ids ?? [],
cron: schedule.cron,
timezone: schedule.timezone,
schedule_at: schedule.schedule_at,
parameters: {
...(schedule.parameters ?? {}),
sync_mode: 'incremental',
},
...(schedule.max_results != null && { max_results: schedule.max_results }),
};
const res = await fetch(
`${EDGES_API_BASE}/actions/linkedin-extract-messages/run/schedule`,
{
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
}
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message ?? `Recreate schedule failed: ${res.status}`);
}
return res.json();
}
End-to-end example: list active Extract Messages schedules, optionally restrict to those using Engagement Identities, then migrate each to incremental by cancel + recreate.
const EDGES_API_BASE = 'https://api.edges.run/v1';
async function migrateExtractMessagesSchedulesToIncremental(apiKey) {
const engagementIds = await getEngagementIdentityIds(apiKey);
if (engagementIds.length === 0) {
console.warn('No Engagement Identities found. Incremental mode requires them.');
return [];
}
const schedules = await listExtractMessagesSchedules(apiKey);
const toMigrate = filterSchedulesByEngagementIdentities(schedules, engagementIds);
const results = [];
for (const s of toMigrate) {
const uid = s.scheduled_run_uid ?? s.uid;
if (!uid) continue;
try {
const newSchedule = await migrateScheduleToIncremental(apiKey, uid);
results.push({ uid, newSchedule });
console.log(`Migrated schedule ${uid}`);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
results.push({ uid, error: message });
console.error(`Failed to migrate ${uid}:`, message);
}
}
return results;
}
// Run migration
migrateExtractMessagesSchedulesToIncremental(process.env.EDGES_API_KEY)
.then((results) => console.log('Migration results:', results))
.catch((err) => console.error('Migration failed:', err));
Summary
| Goal | Approach |
|---|
| New incremental schedule | POST /v1/actions/linkedin-extract-messages/run/schedule with parameters.sync_mode: "incremental" and an Engagement Identity in identity_ids. |
| Async incremental | Same parameters on async run; then call POST /v1/runs/{run_uid}/continue for each subsequent fetch. |
| Existing full-mode schedules → incremental | List schedules (GET /v1/schedules?action_name=linkedin-extract-messages), filter by Engagement Identity if needed, then for each: GET schedule → cancel → recreate with sync_mode: "incremental". |
For more on incremental sync behavior and limits, see the Incremental Sync guide and Continue a run.