diff --git a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.spec.tsx b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.spec.tsx
index 22abe0e55f9..03265dcb69d 100644
--- a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.spec.tsx
+++ b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.spec.tsx
@@ -56,6 +56,11 @@ const service = {
name: 'Incident assistant',
description: 'Investigates production incidents',
enabled: true,
+ schedule: {
+ cron_expression: '0 8 * * 1-5',
+ timezone: 'Europe/Paris',
+ next_run_at: '2026-08-28T06:00:00Z',
+ },
model: {
type: AgenticWorkflowModelType.BEDROCK,
settings: '{"temperature":0.2}',
@@ -154,6 +159,9 @@ describe('AgenticWorkflowSettings views', () => {
expect(screen.getByRole('spinbutton', { name: 'Memory (MiB)' })).toHaveValue(1024)
expect(screen.getByRole('spinbutton', { name: 'GPU' })).toHaveValue(0)
expect(screen.getByRole('spinbutton', { name: 'Storage (GiB)' })).toHaveValue(20)
+ expect(screen.getByText('Schedule agent task')).toBeInTheDocument()
+ expect(screen.getByRole('textbox', { name: 'Cron expression' })).toHaveValue('0 8 * * 1-5')
+ expect(screen.getByText('Europe/Paris')).toBeInTheDocument()
await userEvent.clear(screen.getByRole('textbox', { name: 'Description' }))
await userEvent.type(screen.getByRole('textbox', { name: 'Description' }), 'Updated description')
@@ -169,11 +177,34 @@ describe('AgenticWorkflowSettings views', () => {
model: { type: AgenticWorkflowModelType.BEDROCK, settings: '{"temperature":0.2}' },
outputs: [{ name: 'Audit log', url: null }],
mcp_server_ids: ['mcp-1'],
+ schedule: {
+ cron_expression: '0 8 * * 1-5',
+ timezone: 'Europe/Paris',
+ },
}),
})
)
})
+ it('hides the saved next run when scheduling is disabled in the form', async () => {
+ const { userEvent } = renderWithProviders()
+
+ expect(screen.getByText(/^Next run:/)).toBeInTheDocument()
+
+ await userEvent.click(screen.getByRole('switch', { name: 'Schedule agent task' }))
+
+ expect(screen.queryByText(/^Next run:/)).not.toBeInTheDocument()
+ })
+
+ it('hides the saved next run when the schedule no longer matches the saved value', async () => {
+ const { userEvent } = renderWithProviders()
+
+ await userEvent.clear(screen.getByRole('textbox', { name: 'Cron expression' }))
+ await userEvent.type(screen.getByRole('textbox', { name: 'Cron expression' }), '0 9 * * 1-5')
+
+ expect(screen.queryByText(/^Next run:/)).not.toBeInTheDocument()
+ })
+
it('renders AI configuration without exposing the write-only API key', () => {
renderWithProviders()
diff --git a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx
index 17ac517f938..a56727adba7 100644
--- a/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx
+++ b/libs/domains/service-settings/feature/src/lib/agentic-workflow-settings/agentic-workflow-settings.tsx
@@ -1,12 +1,14 @@
import { useParams } from '@tanstack/react-router'
import { type AgenticWorkflowOutput, type AgenticWorkflowRequest } from 'qovery-typescript-axios'
-import { useForm } from 'react-hook-form'
+import { FormProvider, useForm } from 'react-hook-form'
import { McpServerSetting, useGitTokens, useMcpServers } from '@qovery/domains/organizations/feature'
import { isAgenticWorkflow } from '@qovery/domains/services/data-access'
import {
AgenticWorkflowCodeEditorField,
type AgenticWorkflowGitRepository,
+ AgenticWorkflowScheduleFields,
GitRepositoryCard,
+ isAgenticWorkflowScheduleValid,
isGitRepositoryComplete,
useEditService,
useService,
@@ -25,6 +27,9 @@ interface FormValues {
name: string
description: string
enabled: boolean
+ scheduleEnabled: boolean
+ scheduleCronExpression: string
+ timezone: string
modelApiKey: string
modelSettings: string
agentPrompt: string
@@ -129,6 +134,10 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)
name: service?.name ?? '',
description: service && isAgenticWorkflow(service) ? service.description : '',
enabled: service && isAgenticWorkflow(service) ? service.enabled : false,
+ scheduleEnabled: Boolean(service && isAgenticWorkflow(service) && service.schedule),
+ scheduleCronExpression:
+ service && isAgenticWorkflow(service) ? service.schedule?.cron_expression ?? '0 8 * * 1-5' : '0 8 * * 1-5',
+ timezone: service && isAgenticWorkflow(service) ? service.schedule?.timezone ?? 'Etc/UTC' : 'Etc/UTC',
modelApiKey: '',
modelSettings: service && isAgenticWorkflow(service) ? service.model.settings : '',
agentPrompt: service && isAgenticWorkflow(service) ? service.agent_prompt : '',
@@ -169,6 +178,14 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)
if (!service || !isAgenticWorkflow(service)) return null
const values = form.watch()
+ const savedSchedule = service.schedule
+ const nextRunAt =
+ values.scheduleEnabled &&
+ savedSchedule &&
+ values.scheduleCronExpression === savedSchedule.cron_expression &&
+ values.timezone === savedSchedule.timezone
+ ? savedSchedule.next_run_at
+ : null
const submit = form.handleSubmit((data) => {
const model: AgenticWorkflowRequest['model'] = {
type: service.model.type,
@@ -180,6 +197,12 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)
name: data.name,
description: data.description,
enabled: data.enabled,
+ schedule: data.scheduleEnabled
+ ? {
+ cron_expression: data.scheduleCronExpression,
+ timezone: data.timezone,
+ }
+ : null,
model,
agent_prompt: data.agentPrompt,
project_repositories: formatAgenticWorkflowRepositories(data.repositories),
@@ -221,13 +244,15 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)
const repositoriesValid = values.repositories.every(isGitRepositoryComplete)
const pageValid =
- page === 'ai-configuration'
- ? agenticWorkflowJsonValidation(values.modelSettings) === true
- : page === 'connections'
- ? repositoriesValid && (!values.mcp.trim() || agenticWorkflowJsonValidation(values.mcp) === true)
- : page === 'outputs'
- ? agenticWorkflowOutputsValidation(values.outputs) === true
- : true
+ page === 'general'
+ ? isAgenticWorkflowScheduleValid(values)
+ : page === 'ai-configuration'
+ ? agenticWorkflowJsonValidation(values.modelSettings) === true
+ : page === 'connections'
+ ? repositoriesValid && (!values.mcp.trim() || agenticWorkflowJsonValidation(values.mcp) === true)
+ : page === 'outputs'
+ ? agenticWorkflowOutputsValidation(values.outputs) === true
+ : true
const addRepository = () =>
form.setValue('repositories', [...values.repositories, { repository: '', branch: '' }], { shouldDirty: true })
@@ -258,6 +283,20 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps)
description="Allow this agent task to listen for and process incoming requests."
onChange={(value) => form.setValue('enabled', value, { shouldDirty: true })}
/>
+
+
Schedule
+
+ Configure when this agent task runs automatically in addition to webhook requests.
+
+
+
+
+ {nextRunAt ? (
+
+ Next run: {new Date(nextRunAt).toLocaleString(undefined, { timeZone: values.timezone })}
+
+ ) : null}
+
Resources
diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts
index 7a518746fce..62a3e404f84 100644
--- a/libs/domains/services/feature/src/index.ts
+++ b/libs/domains/services/feature/src/index.ts
@@ -8,6 +8,7 @@ export * from './lib/service-advanced-settings/service-advanced-settings'
export * from './lib/general-setting/general-setting'
export * from './lib/build-settings/build-settings'
export * from './lib/timezone-setting/timezone-setting'
+export * from './lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields'
export * from './lib/general-container-settings/general-container-settings'
export * from './lib/job-general-settings/job-general-settings'
export * from './lib/entrypoint-cmd-inputs/entrypoint-cmd-inputs'
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx
index edee1c7bb26..e929b3725ad 100644
--- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-configuration/agentic-workflow-configuration.tsx
@@ -23,6 +23,7 @@ import {
type AgenticWorkflowOutput,
useAgenticWorkflowCreateContext,
} from '../agentic-workflow-context'
+import { AgenticWorkflowScheduleFields, isAgenticWorkflowScheduleValid } from '../agentic-workflow-schedule-fields'
import { AIModelCards } from './ai-model-cards'
import { GitRepositoryCard } from './git-repository-card'
@@ -235,7 +236,7 @@ export function AgenticWorkflowConfiguration() {
const showNameError = Boolean(dirtyFields.name) && !values.name.trim()
const showModelApiKeyError = Boolean(dirtyFields.modelApiKey) && !values.modelApiKey.trim()
const sectionInvalid: Record = {
- 'service-information': !values.name.trim(),
+ 'service-information': !values.name.trim() || !isAgenticWorkflowScheduleValid(values),
'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError),
connectors: Boolean(mcpJsonError),
'git-repositories': !gitRepositoriesValid,
@@ -254,7 +255,8 @@ export function AgenticWorkflowConfiguration() {
!mcpJsonError &&
outputHeadersErrors.every((error) => !error) &&
!modelSettingsJsonError &&
- variablesValid
+ variablesValid &&
+ isAgenticWorkflowScheduleValid(values)
useEffect(() => {
setCurrentStep(1)
@@ -378,7 +380,11 @@ export function AgenticWorkflowConfiguration() {
description="Start listening and executing this agent task as soon as it is created."
onChange={(value) => form.setValue('workflowEnabled', value, { shouldDirty: true })}
/>
-
+
+
{
expect(formatAgenticWorkflowRequest(values).mcp_server_ids).toEqual(['mcp-1', 'mcp-2'])
})
+ it('sends an optional schedule', () => {
+ expect(formatAgenticWorkflowRequest(values).schedule).toBeNull()
+ expect(formatAgenticWorkflowRequest({ ...values, scheduleEnabled: true }).schedule).toEqual({
+ cron_expression: '0 8 * * 1-5',
+ timezone: 'Europe/Paris',
+ })
+ })
+
it('uses the full URL of a selected Git repository', () => {
const request = formatAgenticWorkflowRequest({
...values,
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.ts
index 209bf0e1b37..e767820a6b7 100644
--- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.ts
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.ts
@@ -30,6 +30,12 @@ export function formatAgenticWorkflowRequest(values: AgenticWorkflowFormData): A
description: values.description,
docker_fragment: values.dockerFragment,
enabled: values.workflowEnabled,
+ schedule: values.scheduleEnabled
+ ? {
+ cron_expression: values.scheduleCronExpression,
+ timezone: values.timezone,
+ }
+ : null,
mcp: values.mcpJson.trim() || undefined,
mcp_server_ids: values.mcpServerIds,
outputs: values.outputs.map((output, index) => ({
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.spec.tsx
new file mode 100644
index 00000000000..45a41c3d3e2
--- /dev/null
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.spec.tsx
@@ -0,0 +1,39 @@
+import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form'
+import { renderWithProviders, screen } from '@qovery/shared/util-tests'
+import { AgenticWorkflowScheduleFields, isAgenticWorkflowScheduleValid } from './agentic-workflow-schedule-fields'
+
+describe('AgenticWorkflowScheduleFields', () => {
+ it('links to the CRON expression builder when scheduling is enabled', () => {
+ renderWithProviders(
+ wrapWithReactHookForm(, {
+ defaultValues: {
+ scheduleEnabled: true,
+ scheduleCronExpression: '0 8 * * 1-5',
+ timezone: 'Etc/UTC',
+ },
+ })
+ )
+
+ expect(screen.getByRole('link', { name: 'CRON expression builder' })).toHaveAttribute(
+ 'href',
+ 'https://crontab.guru/'
+ )
+ })
+
+ it('requires a valid cron expression only when scheduling is enabled', () => {
+ expect(
+ isAgenticWorkflowScheduleValid({
+ scheduleEnabled: false,
+ scheduleCronExpression: 'invalid',
+ timezone: 'Etc/UTC',
+ })
+ ).toBe(true)
+ expect(
+ isAgenticWorkflowScheduleValid({
+ scheduleEnabled: true,
+ scheduleCronExpression: 'invalid',
+ timezone: 'Etc/UTC',
+ })
+ ).toBe(false)
+ })
+})
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.tsx
new file mode 100644
index 00000000000..7b20cdaebac
--- /dev/null
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.tsx
@@ -0,0 +1,65 @@
+import { Controller, useFormContext } from 'react-hook-form'
+import { ExternalLink, InputText, InputToggle } from '@qovery/shared/ui'
+import { formatCronExpression } from '@qovery/shared/util-js'
+import { TimezoneSetting } from '../../timezone-setting/timezone-setting'
+
+export interface AgenticWorkflowScheduleFormValues {
+ scheduleEnabled: boolean
+ scheduleCronExpression: string
+ timezone: string
+}
+
+export function isAgenticWorkflowScheduleValid(values: AgenticWorkflowScheduleFormValues) {
+ return !values.scheduleEnabled || Boolean(formatCronExpression(values.scheduleCronExpression))
+}
+
+export function AgenticWorkflowScheduleFields() {
+ const { control, setValue, watch } = useFormContext()
+ const scheduleEnabled = watch('scheduleEnabled')
+ const scheduleCronExpression = watch('scheduleCronExpression')
+ const timezone = watch('timezone')
+ const formattedSchedule = formatCronExpression(scheduleCronExpression)
+
+ return (
+
+
setValue('scheduleEnabled', value, { shouldDirty: true, shouldValidate: true })}
+ />
+ {scheduleEnabled ? (
+
+
+
+ CRON expression builder
+
+
+
+ Boolean(formatCronExpression(value)) || 'Invalid cron expression.',
+ }}
+ render={({ field, fieldState: { error } }) => (
+
+ )}
+ />
+
+
+
+ ) : null}
+
+ )
+}
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx
index 1174f192c1a..69c6733d44c 100644
--- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.spec.tsx
@@ -126,6 +126,7 @@ describe('AgenticWorkflowSummary', () => {
expect(screen.getByText(validValues.mcpJson ?? '')).toBeInTheDocument()
expect(screen.getByText('Documentation, Tickets')).toBeInTheDocument()
expect(screen.getByText('1 webhook')).toBeInTheDocument()
+ expect(screen.queryByRole('button', { name: 'Edit Schedule' })).not.toBeInTheDocument()
expect(mockNavigate).not.toHaveBeenCalledWith({ to: '/create/agentic-workflow/configuration' })
})
@@ -137,6 +138,15 @@ describe('AgenticWorkflowSummary', () => {
expect(mockNavigate).toHaveBeenCalledWith({ to: '/create/agentic-workflow/configuration' })
})
+ it('should redirect an invalid enabled schedule back to configuration', () => {
+ renderSummary({
+ scheduleEnabled: true,
+ scheduleCronExpression: 'invalid',
+ })
+
+ expect(mockNavigate).toHaveBeenCalledWith({ to: '/create/agentic-workflow/configuration' })
+ })
+
it('should create the agentic workflow and navigate back to the environment overview', async () => {
const { userEvent } = renderSummary()
diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx
index 0d3c7c678b2..c10c72218e2 100644
--- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx
+++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx
@@ -17,6 +17,7 @@ import {
useAgenticWorkflowCreateContext,
} from './agentic-workflow-context'
import { formatAgenticWorkflowRequest } from './agentic-workflow-request'
+import { isAgenticWorkflowScheduleValid } from './agentic-workflow-schedule-fields'
function truncateSummary(value: string) {
if (!value.trim()) return '-'
@@ -106,6 +107,7 @@ export function AgenticWorkflowSummary() {
!values.agentPrompt.trim() ||
hasIncompleteGitRepository(values) ||
hasIncompleteOutput(values) ||
+ !isAgenticWorkflowScheduleValid(values) ||
!variablesValid
) {
navigate({ to: `${creationFlowUrl}/configuration` })
@@ -168,6 +170,13 @@ export function AgenticWorkflowSummary() {
+
+ {values.scheduleEnabled ? (
+ <>
+
+
+ >
+ ) : null}
handleEditSection('ai-model')}>
diff --git a/package.json b/package.json
index f8a30b0b7f6..5b310991000 100644
--- a/package.json
+++ b/package.json
@@ -77,7 +77,7 @@
"mermaid": "11.6.0",
"monaco-editor": "0.53.0",
"posthog-js": "1.345.1",
- "qovery-typescript-axios": "1.1.958",
+ "qovery-typescript-axios": "1.1.962",
"react": "18.3.1",
"react-country-flag": "3.0.2",
"react-datepicker": "4.12.0",
diff --git a/yarn.lock b/yarn.lock
index 4bcd007c6e8..772e780d114 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6343,7 +6343,7 @@ __metadata:
prettier: 3.2.5
prettier-plugin-tailwindcss: 0.5.14
pretty-quick: 4.0.0
- qovery-typescript-axios: 1.1.958
+ qovery-typescript-axios: 1.1.962
qovery-ws-typescript-axios: 0.1.644
react: 18.3.1
react-country-flag: 3.0.2
@@ -25863,12 +25863,12 @@ __metadata:
languageName: node
linkType: hard
-"qovery-typescript-axios@npm:1.1.958":
- version: 1.1.958
- resolution: "qovery-typescript-axios@npm:1.1.958"
+"qovery-typescript-axios@npm:1.1.962":
+ version: 1.1.962
+ resolution: "qovery-typescript-axios@npm:1.1.962"
dependencies:
axios: 1.18.1
- checksum: 91757e4ceb42ef4ec005a4d6b97ee39ec77a44958f4225292b38ed78cdbd52da373fed9cc688608da967147af2246d1b508067d1580a172af2caf2a21d6f57df
+ checksum: d26915192af2b90c62fd4f559ab5ba3ca9b7bc0ed9f9e3a8c8d7613de3b006d3e46699c07b83239967e60f6e40149e1c2259ee385bc689e3d851df8ca5ecaabe
languageName: node
linkType: hard