Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions src/cloud/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,14 @@ export type {
RickyLinearSession,
SessionEndReason,
} from './linear-agent-types.js';

export {
listRickyWorkflowSchedules,
scheduleRickyWorkflow,
} from './workflow-schedules.js';
export type {
ListRickyWorkflowSchedulesResult,
RickyWorkflowSchedule,
ScheduleRickyWorkflowOptions,
ScheduleRickyWorkflowResult,
} from './workflow-schedules.js';
73 changes: 73 additions & 0 deletions src/cloud/api/workflow-schedules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export type RickyWorkflowScheduleType = 'cron' | 'once';

export interface RickyWorkflowSchedule {
id: string;
name: string;
description?: string | null;
scheduleType: RickyWorkflowScheduleType;
cronExpression?: string | null;
scheduledAt?: string | Date | null;
timezone?: string;
status: string;
lastTriggeredRunId?: string | null;
lastTriggeredAt?: string | Date | null;
createdAt?: string | Date;
updatedAt?: string | Date;
}

export interface ScheduleRickyWorkflowOptions {
name?: string;
cronExpression?: string;
scheduledAt?: string;
timezone?: string;
}

export interface ScheduleRickyWorkflowResult {
schedule: RickyWorkflowSchedule;
}

export interface ListRickyWorkflowSchedulesResult {
schedules: RickyWorkflowSchedule[];
}

type RelaySdkWorkflowSchedules = {
scheduleWorkflow: (
workflowPath: string,
options: {
name?: string;
cron?: string;
at?: string;
timezone?: string;
},
) => Promise<RickyWorkflowSchedule>;
listWorkflowSchedules: () => Promise<RickyWorkflowSchedule[]>;
};

async function relayWorkflowSchedules(): Promise<RelaySdkWorkflowSchedules> {
const sdk = await import('@agent-relay/sdk/workflows') as unknown as Partial<RelaySdkWorkflowSchedules>;
if (typeof sdk.scheduleWorkflow !== 'function' || typeof sdk.listWorkflowSchedules !== 'function') {
throw new Error(
'Installed @agent-relay/sdk does not expose workflow scheduling yet. Upgrade to the Relay SDK version that includes scheduleWorkflow.',
);
}
return sdk as RelaySdkWorkflowSchedules;
}

export async function scheduleRickyWorkflow(
workflowPath: string,
options: ScheduleRickyWorkflowOptions,
): Promise<ScheduleRickyWorkflowResult> {
const sdk = await relayWorkflowSchedules();
const schedule = await sdk.scheduleWorkflow(workflowPath, {
...(options.name ? { name: options.name } : {}),
...(options.cronExpression ? { cron: options.cronExpression } : {}),
...(options.scheduledAt ? { at: options.scheduledAt } : {}),
...(options.timezone ? { timezone: options.timezone } : {}),
});
return { schedule };
}

export async function listRickyWorkflowSchedules(): Promise<ListRickyWorkflowSchedulesResult> {
const sdk = await relayWorkflowSchedules();
return { schedules: await sdk.listWorkflowSchedules() };
}
102 changes: 102 additions & 0 deletions src/surfaces/cli/commands/cli-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,43 @@ describe('parseArgs', () => {
connectTarget: 'linear',
});
});

it('parses schedule commands', () => {
expect(parseArgs([
'schedule',
'workflows/generated/package-checks.ts',
'--cron',
'0 9 * * 1',
'--name',
'Package checks',
'--timezone',
'America/New_York',
])).toMatchObject({
command: 'schedule',
surface: 'schedule',
artifact: 'workflows/generated/package-checks.ts',
cronExpression: '0 9 * * 1',
workflowName: 'Package checks',
timezone: 'America/New_York',
});
expect(parseArgs(['schedule', 'workflows/generated/package-checks.ts', '--at', '2026-05-10T09:00:00Z'])).toMatchObject({
command: 'schedule',
scheduledAt: '2026-05-10T09:00:00Z',
});
expect(parseArgs(['schedule', 'list', '--json'])).toEqual({
command: 'schedules',
surface: 'schedule',
json: true,
});
expect(parseArgs(['schedules', '--json'])).toEqual({
command: 'schedules',
surface: 'schedule',
json: true,
});
expect(parseArgs(['schedule', 'workflows/generated/package-checks.ts'])).toMatchObject({
errors: ['schedule requires --cron or --at.'],
});
});
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -400,6 +437,8 @@ describe('renderHelp', () => {
expect(helpText).not.toContain('ricky workflow --spec-file <path> --mode cloud --run');
expect(helpText).toContain('ricky run <path> --background');
expect(helpText).toContain('ricky run <path> --cloud');
expect(helpText).toContain('ricky schedule <artifact> --cron "0 * * * *"');
expect(helpText).toContain('ricky schedules');
expect(helpText).toContain('ricky run <artifact> --start-from <step>');
expect(helpText).toContain('ricky status --run <run-id>');
expect(helpText).toContain('Without --run: artifact path on disk');
Expand Down Expand Up @@ -2669,6 +2708,69 @@ describe('cliMain', () => {
expect(runner).not.toHaveBeenCalled();
});

it('schedules a cloud workflow without invoking the interactive runner', async () => {
const runner = vi.fn();
const scheduleWorkflow = vi.fn().mockResolvedValue({
schedule: {
id: 'sched-1',
name: 'Package checks',
scheduleType: 'cron',
cronExpression: '0 9 * * 1',
timezone: 'UTC',
status: 'active',
},
});

const result = await cliMain({
argv: [
'schedule',
'workflows/generated/package-checks.ts',
'--cron',
'0 9 * * 1',
'--name',
'Package checks',
],
runInteractive: runner,
scheduleWorkflow,
});

expect(result.exitCode).toBe(0);
expect(scheduleWorkflow).toHaveBeenCalledWith('workflows/generated/package-checks.ts', {
name: 'Package checks',
cronExpression: '0 9 * * 1',
});
expect(result.output.join('\n')).toContain('Ricky cloud schedule created');
expect(result.output.join('\n')).toContain('sched-1');
expect(runner).not.toHaveBeenCalled();
});

it('lists cloud workflow schedules without invoking the interactive runner', async () => {
const runner = vi.fn();
const listWorkflowSchedules = vi.fn().mockResolvedValue({
schedules: [
{
id: 'sched-1',
name: 'Package checks',
scheduleType: 'cron',
cronExpression: '0 9 * * 1',
status: 'active',
},
],
});

const result = await cliMain({
argv: ['schedules', '--json'],
runInteractive: runner,
listWorkflowSchedules,
});

expect(result.exitCode).toBe(0);
expect(JSON.parse(result.output.join('\n'))).toMatchObject({
schedules: [{ id: 'sched-1', name: 'Package checks' }],
});
expect(runner).not.toHaveBeenCalled();
});

// -------------------------------------------------------------------------
// Proof: spec fixture coverage — inline, spec-file, stdin, missing, recovery
// -------------------------------------------------------------------------
Expand Down
Loading
Loading