Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
945c9b4
feat: workflows
felipefreitag Mar 3, 2026
959561e
chore: bump version
felipefreitag Mar 3, 2026
610ca1f
feat: add events and patch workflow
felipefreitag Mar 3, 2026
41ea962
fix: update workflow create response
felipefreitag Mar 3, 2026
51494bd
fix: update event schema validation
felipefreitag Mar 4, 2026
dfcf123
fix: linter
felipefreitag Mar 4, 2026
c35348c
fix: update types
felipefreitag Mar 4, 2026
c77997b
feat: add workflow runs list and details
felipefreitag Mar 5, 2026
ebfd3d9
feat: add workflow run steps
felipefreitag Mar 5, 2026
2a52217
fix: remove implementation details
felipefreitag Mar 5, 2026
3c18f09
feat: add steps payload
felipefreitag Mar 6, 2026
37bdf58
feat: improve workflow types
felipefreitag Mar 6, 2026
c95c1cd
chore: bump package
felipefreitag Mar 6, 2026
6044ed9
fix:lint
felipefreitag Mar 6, 2026
14f96ea
fix: types
felipefreitag Mar 6, 2026
c02d756
fix: remove type repetition
felipefreitag Mar 6, 2026
74abc72
fix: lint
felipefreitag Mar 6, 2026
8ee5942
move to automations
vcapretz Mar 25, 2026
ae6d889
use automations endpoint
vcapretz Mar 25, 2026
2b845bd
chore: bump package version
CarolinaMoraes Mar 25, 2026
19e5642
feat: remove workflows namespace from SDK (#898)
vcapretz Mar 30, 2026
a1799f9
fix send event
vcapretz Mar 30, 2026
1e57f4f
new version
vcapretz Mar 30, 2026
2096c66
new version
vcapretz Apr 9, 2026
b21dd33
chore: remove automation-run-steps endpoints that don't exist in the …
vcapretz Apr 9, 2026
027ee94
ref to key
zenorocha Apr 10, 2026
3ab7349
feat: update code to correspond to automation API changes (#917)
CarolinaMoraes Apr 10, 2026
2455ebf
refactor: edge type (#919)
bukinoshita Apr 10, 2026
d8fbb20
refactor: rename edges (#920)
bukinoshita Apr 10, 2026
ab25d1f
chore: automations breaking changes (#918)
isabellaaquino Apr 10, 2026
e145638
chore: bump version (#923)
isabellaaquino Apr 10, 2026
7ef5d29
fix: template object structure (#924)
zenorocha Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "resend",
"version": "6.10.0",
"version": "6.10.0-preview-workflows.7",
"description": "Node.js library for the Resend API",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
Expand Down
235 changes: 235 additions & 0 deletions src/automation-runs/automation-runs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import createFetchMock from 'vitest-fetch-mock';
import { Resend } from '../resend';
import {
mockErrorResponse,
mockSuccessResponse,
} from '../test-utils/mock-fetch';
import type {
GetAutomationRunOptions,
GetAutomationRunResponseSuccess,
} from './interfaces/get-automation-run.interface';
import type {
ListAutomationRunsOptions,
ListAutomationRunsResponseSuccess,
} from './interfaces/list-automation-runs.interface';

const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();

afterEach(() => fetchMock.resetMocks());
afterAll(() => fetchMocker.disableMocks());

describe('get', () => {
it('gets an automation run', async () => {
const options: GetAutomationRunOptions = {
automationId: 'wf_123',
runId: 'wr_456',
};
const response: GetAutomationRunResponseSuccess = {
object: 'automation_run',
id: 'wr_456',
status: 'completed',
started_at: '2024-01-01T00:00:00.000Z',
completed_at: '2024-01-01T00:01:00.000Z',
created_at: '2024-01-01T00:00:00.000Z',
steps: [
{
key: 'trigger_1',
type: 'trigger',
status: 'completed',
output: null,
error: null,
started_at: '2024-01-01T00:00:00.000Z',
completed_at: '2024-01-01T00:00:01.000Z',
created_at: '2024-01-01T00:00:00.000Z',
},
],
};

mockSuccessResponse(response, {});

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
Comment thread
zenorocha marked this conversation as resolved.
await expect(
resend.automations.runs.get(options),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: API Key Permission Check SDK Methods

The new Resend SDK methods for automation run get/list require confirming production API keys include the necessary permissions for automation run read/list operations, per the API Key Permission Check rule.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/automation-runs/automation-runs.spec.ts, line 45:

<comment>The new Resend SDK methods for automation run get/list require confirming production API keys include the necessary permissions for automation run read/list operations, per the API Key Permission Check rule.</comment>

<file context>
@@ -0,0 +1,202 @@
+
+    const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
+    await expect(
+      resend.automations.runs.get(options),
+    ).resolves.toMatchInlineSnapshot(`
+        {
</file context>

).resolves.toMatchInlineSnapshot(`
{
"data": {
"completed_at": "2024-01-01T00:01:00.000Z",
"created_at": "2024-01-01T00:00:00.000Z",
"id": "wr_456",
"object": "automation_run",
"started_at": "2024-01-01T00:00:00.000Z",
"status": "completed",
"steps": [
{
"completed_at": "2024-01-01T00:00:01.000Z",
"created_at": "2024-01-01T00:00:00.000Z",
"error": null,
"key": "trigger_1",
"output": null,
"started_at": "2024-01-01T00:00:00.000Z",
"status": "completed",
"type": "trigger",
},
],
},
"error": null,
"headers": {
"content-type": "application/json",
},
}
`);
});

it('returns error', async () => {
const options: GetAutomationRunOptions = {
automationId: 'wf_123',
runId: 'wr_invalid',
};

mockErrorResponse(
{ name: 'not_found', message: 'Automation run not found' },
{},
);

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
const result = await resend.automations.runs.get(options);
expect(result.error).not.toBeNull();
});
});

describe('list', () => {
it('lists automation runs', async () => {
const options: ListAutomationRunsOptions = {
automationId: 'wf_123',
};
const response: ListAutomationRunsResponseSuccess = {
object: 'list',
data: [
{
id: 'wr_456',
status: 'completed',
started_at: '2024-01-01T00:00:00.000Z',
completed_at: '2024-01-01T00:01:00.000Z',
created_at: '2024-01-01T00:00:00.000Z',
},
],
has_more: false,
};

mockSuccessResponse(response, {});

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
await expect(
resend.automations.runs.list(options),
).resolves.toMatchInlineSnapshot(`
{
"data": {
"data": [
{
"completed_at": "2024-01-01T00:01:00.000Z",
"created_at": "2024-01-01T00:00:00.000Z",
"id": "wr_456",
"started_at": "2024-01-01T00:00:00.000Z",
"status": "completed",
},
],
"has_more": false,
"object": "list",
},
"error": null,
"headers": {
"content-type": "application/json",
},
}
`);
});

it('lists automation runs with pagination', async () => {
const options: ListAutomationRunsOptions = {
automationId: 'wf_123',
limit: 1,
after: 'wr_cursor',
};
const response: ListAutomationRunsResponseSuccess = {
object: 'list',
data: [
{
id: 'wr_789',
status: 'running',
started_at: '2024-01-02T00:00:00.000Z',
completed_at: null,
created_at: '2024-01-02T00:00:00.000Z',
},
],
has_more: true,
};

mockSuccessResponse(response, {});

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
await expect(
resend.automations.runs.list(options),
).resolves.toMatchInlineSnapshot(`
{
"data": {
"data": [
{
"completed_at": null,
"created_at": "2024-01-02T00:00:00.000Z",
"id": "wr_789",
"started_at": "2024-01-02T00:00:00.000Z",
"status": "running",
},
],
"has_more": true,
"object": "list",
},
"error": null,
"headers": {
"content-type": "application/json",
},
}
`);
});

it('lists automation runs with status filter', async () => {
const options: ListAutomationRunsOptions = {
automationId: 'wf_123',
status: ['running', 'failed'],
};
const response: ListAutomationRunsResponseSuccess = {
object: 'list',
data: [],
has_more: false,
};

mockSuccessResponse(response, {});

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
await resend.automations.runs.list(options);

expect(fetchMock).toHaveBeenCalledWith(
'https://api.resend.com/automations/wf_123/runs?status=running%2Cfailed',
expect.objectContaining({
method: 'GET',
headers: expect.any(Headers),
}),
);
});

it('returns error', async () => {
const options: ListAutomationRunsOptions = {
automationId: 'wf_invalid',
};

mockErrorResponse(
{ name: 'not_found', message: 'Automation not found' },
{},
);

const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
const result = await resend.automations.runs.list(options);
expect(result.error).not.toBeNull();
});
});
47 changes: 47 additions & 0 deletions src/automation-runs/automation-runs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { buildPaginationQuery } from '../common/utils/build-pagination-query';
import type { Resend } from '../resend';
import type {
GetAutomationRunOptions,
GetAutomationRunResponse,
GetAutomationRunResponseSuccess,
} from './interfaces/get-automation-run.interface';
import type {
ListAutomationRunsOptions,
ListAutomationRunsResponse,
ListAutomationRunsResponseSuccess,
} from './interfaces/list-automation-runs.interface';

export class AutomationRuns {
constructor(private readonly resend: Resend) {}

async get(
options: GetAutomationRunOptions,
): Promise<GetAutomationRunResponse> {
const data = await this.resend.get<GetAutomationRunResponseSuccess>(
`/automations/${options.automationId}/runs/${options.runId}`,
);
return data;
}

async list(
options: ListAutomationRunsOptions,
): Promise<ListAutomationRunsResponse> {
const queryString = buildPaginationQuery(options);
const searchParams = new URLSearchParams(queryString);

if (options.status) {
const statusValue = Array.isArray(options.status)
? options.status.join(',')
: options.status;
searchParams.set('status', statusValue);
}
Comment on lines +32 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid setting status when it is an empty array; the current truthy check serializes [] as status=.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/automation-runs/automation-runs.ts, line 32:

<comment>Avoid setting `status` when it is an empty array; the current truthy check serializes `[]` as `status=`.</comment>

<file context>
@@ -27,8 +27,18 @@ export class AutomationRuns {
-      ? `/automations/${options.automationId}/runs?${queryString}`
+    const searchParams = new URLSearchParams(queryString);
+
+    if (options.status) {
+      const statusValue = Array.isArray(options.status)
+        ? options.status.join(',')
</file context>
Suggested change
if (options.status) {
const statusValue = Array.isArray(options.status)
? options.status.join(',')
: options.status;
searchParams.set('status', statusValue);
}
if (
options.status !== undefined &&
(!Array.isArray(options.status) || options.status.length > 0)
) {
const statusValue = Array.isArray(options.status)
? options.status.join(',')
: options.status;
searchParams.set('status', statusValue);
}


const qs = searchParams.toString();
const url = qs
? `/automations/${options.automationId}/runs?${qs}`
: `/automations/${options.automationId}/runs`;

const data = await this.resend.get<ListAutomationRunsResponseSuccess>(url);
return data;
}
}
41 changes: 41 additions & 0 deletions src/automation-runs/interfaces/automation-run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { AutomationStepType } from '../../automations/interfaces/automation-step.interface';

export type AutomationRunStatus =
| 'running'
| 'completed'
| 'failed'
| 'cancelled';

export type AutomationRunStepStatus =
| 'pending'
| 'running'
| 'completed'
| 'failed'
| 'skipped'
| 'waiting';

export interface AutomationRunStep {
key: string;
type: AutomationStepType;
status: AutomationRunStepStatus;
output: Record<string, unknown> | null;
error: Record<string, unknown> | null;
started_at: string | null;
completed_at: string | null;
created_at: string;
}

export interface AutomationRun {
object: 'automation_run';
id: string;
status: AutomationRunStatus;
started_at: string | null;
completed_at: string | null;
created_at: string;
steps: AutomationRunStep[];
}

export type AutomationRunItem = Pick<
AutomationRun,
'id' | 'status' | 'started_at' | 'completed_at' | 'created_at'
>;
12 changes: 12 additions & 0 deletions src/automation-runs/interfaces/get-automation-run.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Response } from '../../interfaces';
import type { AutomationRun } from './automation-run';

export interface GetAutomationRunOptions {
automationId: string;
runId: string;
}

export type GetAutomationRunResponseSuccess = AutomationRun;

export type GetAutomationRunResponse =
Response<GetAutomationRunResponseSuccess>;
3 changes: 3 additions & 0 deletions src/automation-runs/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './automation-run';
export * from './get-automation-run.interface';
export * from './list-automation-runs.interface';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type {
PaginatedData,
PaginationOptions,
} from '../../common/interfaces/pagination-options.interface';
import type { Response } from '../../interfaces';
import type { AutomationRunItem, AutomationRunStatus } from './automation-run';

export type ListAutomationRunsOptions = PaginationOptions & {
automationId: string;
status?: AutomationRunStatus | AutomationRunStatus[];
};

export type ListAutomationRunsResponseSuccess = PaginatedData<
AutomationRunItem[]
>;

export type ListAutomationRunsResponse =
Response<ListAutomationRunsResponseSuccess>;
Loading
Loading