From bc61e334b4a7e0d2bf9c8e0ee3feef2d25debde9 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 27 Aug 2026 15:14:50 +0200 Subject: [PATCH 1/3] feat(agent-task): add workflow scheduling --- .../agentic-workflow-settings.spec.tsx | 12 +++++ .../agentic-workflow-settings.tsx | 47 +++++++++++++--- libs/domains/services/feature/src/index.ts | 1 + .../agentic-workflow-configuration.tsx | 21 +++++++- .../agentic-workflow-context.tsx | 7 +++ .../agentic-workflow-request.spec.ts | 11 ++++ .../agentic-workflow-request.ts | 6 +++ .../agentic-workflow-schedule-fields.tsx | 54 +++++++++++++++++++ .../agentic-workflow-summary.tsx | 10 ++++ package.json | 2 +- yarn.lock | 10 ++-- 11 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.tsx 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..76c33662b56 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,6 +177,10 @@ 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', + }, }), }) ) 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..f19bd11e57b 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,11 +1,12 @@ 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, isGitRepositoryComplete, useEditService, @@ -14,6 +15,7 @@ import { import { SettingsHeading } from '@qovery/shared/console-shared' import { Button, Heading, Icon, InputText, InputTextArea, InputToggle, Section } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/util-hooks' +import { formatCronExpression } from '@qovery/shared/util-js' type SettingsPage = 'general' | 'ai-configuration' | 'connections' | 'outputs' | 'governance' @@ -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 : '', @@ -180,6 +189,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 +236,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' + ? !values.scheduleEnabled || Boolean(formatCronExpression(values.scheduleCronExpression)) + : 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 +275,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. +

+ + + + {service.schedule?.next_run_at ? ( +

+ Next run: {new Date(service.schedule.next_run_at).toLocaleString()} +

+ ) : 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..6f8eb212943 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 @@ -17,17 +17,20 @@ import { InputToggle, Section, } from '@qovery/shared/ui' +import { formatCronExpression } from '@qovery/shared/util-js' import { type AgenticWorkflowConfigurationSection, type AgenticWorkflowGitRepository, type AgenticWorkflowOutput, useAgenticWorkflowCreateContext, } from '../agentic-workflow-context' +import { AgenticWorkflowScheduleFields } from '../agentic-workflow-schedule-fields' import { AIModelCards } from './ai-model-cards' import { GitRepositoryCard } from './git-repository-card' const sectionOrder: AgenticWorkflowConfigurationSection[] = [ 'service-information', + 'schedule', 'ai-model', 'connectors', 'git-repositories', @@ -59,6 +62,7 @@ function isSectionCompleted( function getSectionTitle(section: AgenticWorkflowConfigurationSection) { const titles: Record = { 'service-information': 'Service information', + schedule: 'Schedule', 'ai-model': 'AI model', connectors: 'MCPs', 'git-repositories': 'Git repositories', @@ -236,6 +240,9 @@ export function AgenticWorkflowConfiguration() { const showModelApiKeyError = Boolean(dirtyFields.modelApiKey) && !values.modelApiKey.trim() const sectionInvalid: Record = { 'service-information': !values.name.trim(), + schedule: + values.scheduleEnabled && + (!values.scheduleCronExpression.trim() || !formatCronExpression(values.scheduleCronExpression)), 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), connectors: Boolean(mcpJsonError), 'git-repositories': !gitRepositoriesValid, @@ -254,7 +261,8 @@ export function AgenticWorkflowConfiguration() { !mcpJsonError && outputHeadersErrors.every((error) => !error) && !modelSettingsJsonError && - variablesValid + variablesValid && + (!values.scheduleEnabled || Boolean(formatCronExpression(values.scheduleCronExpression))) useEffect(() => { setCurrentStep(1) @@ -381,6 +389,17 @@ export function AgenticWorkflowConfiguration() { + + + + + } diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx index c1d954aca07..efb5c91d0b1 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-context.tsx @@ -60,6 +60,7 @@ export const agenticWorkflowCreationSteps: { title: string }[] = [{ title: 'Conf export type AgenticWorkflowConfigurationSection = | 'service-information' + | 'schedule' | 'ai-model' | 'connectors' | 'git-repositories' @@ -92,6 +93,9 @@ export interface AgenticWorkflowFormData { memory: string storage: string workflowEnabled: boolean + scheduleEnabled: boolean + scheduleCronExpression: string + timezone: string aiModel: AgenticWorkflowModelType webhookEnabled: boolean mcpServerIds: string[] @@ -147,6 +151,9 @@ export function AgenticWorkflowCreationFlow({ children, creationFlowUrl, onExit memory: '2048', storage: '10', workflowEnabled: true, + scheduleEnabled: false, + scheduleCronExpression: '0 8 * * 1-5', + timezone: 'Etc/UTC', aiModel: AgenticWorkflowModelType.CLAUDE, webhookEnabled: true, mcpServerIds: [], diff --git a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.spec.ts b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.spec.ts index 895b7eea31b..eaccf6e2caa 100644 --- a/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.spec.ts +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-request.spec.ts @@ -9,6 +9,9 @@ const values: AgenticWorkflowFormData = { memory: '2048', storage: '10', workflowEnabled: true, + scheduleEnabled: false, + scheduleCronExpression: '0 8 * * 1-5', + timezone: 'Europe/Paris', aiModel: AgenticWorkflowModelType.CLAUDE, webhookEnabled: true, mcpServerIds: ['mcp-1', 'mcp-2'], @@ -27,6 +30,14 @@ describe('formatAgenticWorkflowRequest', () => { 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.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.tsx new file mode 100644 index 00000000000..c077739f444 --- /dev/null +++ b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.tsx @@ -0,0 +1,54 @@ +import { Controller, useFormContext } from 'react-hook-form' +import { 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 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 ? ( +
+ 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.tsx b/libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-summary.tsx index 0d3c7c678b2..1ffab23d300 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 @@ -170,6 +170,16 @@ export function AgenticWorkflowSummary() { + handleEditSection('schedule')}> + + {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 From 51eef76fa4ccd0ba53e532972cef6c173555ba54 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 27 Aug 2026 15:28:43 +0200 Subject: [PATCH 2/3] fix(agent-task): align schedule form states --- .../agentic-workflow-settings.spec.tsx | 19 ++++++++++ .../agentic-workflow-settings.tsx | 16 ++++++--- .../agentic-workflow-configuration.tsx | 19 +++------- .../agentic-workflow-schedule-fields.spec.tsx | 36 +++++++++++++++++++ .../agentic-workflow-schedule-fields.tsx | 7 +++- .../agentic-workflow-summary.spec.tsx | 9 +++++ .../agentic-workflow-summary.tsx | 2 ++ 7 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 libs/domains/services/feature/src/lib/service-creation-flow/agentic-workflow/agentic-workflow-schedule-fields.spec.tsx 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 76c33662b56..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 @@ -186,6 +186,25 @@ describe('AgenticWorkflowSettings views', () => { ) }) + 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 f19bd11e57b..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 @@ -8,6 +8,7 @@ import { type AgenticWorkflowGitRepository, AgenticWorkflowScheduleFields, GitRepositoryCard, + isAgenticWorkflowScheduleValid, isGitRepositoryComplete, useEditService, useService, @@ -15,7 +16,6 @@ import { import { SettingsHeading } from '@qovery/shared/console-shared' import { Button, Heading, Icon, InputText, InputTextArea, InputToggle, Section } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/util-hooks' -import { formatCronExpression } from '@qovery/shared/util-js' type SettingsPage = 'general' | 'ai-configuration' | 'connections' | 'outputs' | 'governance' @@ -178,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, @@ -237,7 +245,7 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps) const repositoriesValid = values.repositories.every(isGitRepositoryComplete) const pageValid = page === 'general' - ? !values.scheduleEnabled || Boolean(formatCronExpression(values.scheduleCronExpression)) + ? isAgenticWorkflowScheduleValid(values) : page === 'ai-configuration' ? agenticWorkflowJsonValidation(values.modelSettings) === true : page === 'connections' @@ -283,9 +291,9 @@ export function AgenticWorkflowSettings({ page }: AgenticWorkflowSettingsProps) - {service.schedule?.next_run_at ? ( + {nextRunAt ? (

- Next run: {new Date(service.schedule.next_run_at).toLocaleString()} + Next run: {new Date(nextRunAt).toLocaleString(undefined, { timeZone: values.timezone })}

) : null}
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 6f8eb212943..06187dc1e2e 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 @@ -17,14 +17,13 @@ import { InputToggle, Section, } from '@qovery/shared/ui' -import { formatCronExpression } from '@qovery/shared/util-js' import { type AgenticWorkflowConfigurationSection, type AgenticWorkflowGitRepository, type AgenticWorkflowOutput, useAgenticWorkflowCreateContext, } from '../agentic-workflow-context' -import { AgenticWorkflowScheduleFields } from '../agentic-workflow-schedule-fields' +import { AgenticWorkflowScheduleFields, isAgenticWorkflowScheduleValid } from '../agentic-workflow-schedule-fields' import { AIModelCards } from './ai-model-cards' import { GitRepositoryCard } from './git-repository-card' @@ -240,9 +239,7 @@ export function AgenticWorkflowConfiguration() { const showModelApiKeyError = Boolean(dirtyFields.modelApiKey) && !values.modelApiKey.trim() const sectionInvalid: Record = { 'service-information': !values.name.trim(), - schedule: - values.scheduleEnabled && - (!values.scheduleCronExpression.trim() || !formatCronExpression(values.scheduleCronExpression)), + schedule: !isAgenticWorkflowScheduleValid(values), 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), connectors: Boolean(mcpJsonError), 'git-repositories': !gitRepositoriesValid, @@ -262,7 +259,7 @@ export function AgenticWorkflowConfiguration() { outputHeadersErrors.every((error) => !error) && !modelSettingsJsonError && variablesValid && - (!values.scheduleEnabled || Boolean(formatCronExpression(values.scheduleCronExpression))) + isAgenticWorkflowScheduleValid(values) useEffect(() => { setCurrentStep(1) @@ -390,14 +387,8 @@ export function AgenticWorkflowConfiguration() { - - + + { + it('focuses the schedule toggle when requested', () => { + renderWithProviders( + wrapWithReactHookForm(, { + defaultValues: { + scheduleEnabled: false, + scheduleCronExpression: '0 8 * * 1-5', + timezone: 'Etc/UTC', + }, + }) + ) + + expect(screen.getByRole('switch', { name: 'Schedule agent task' })).toHaveFocus() + }) + + 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 index c077739f444..ed7903aff08 100644 --- 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 @@ -9,7 +9,11 @@ export interface AgenticWorkflowScheduleFormValues { timezone: string } -export function AgenticWorkflowScheduleFields() { +export function isAgenticWorkflowScheduleValid(values: AgenticWorkflowScheduleFormValues) { + return !values.scheduleEnabled || Boolean(formatCronExpression(values.scheduleCronExpression)) +} + +export function AgenticWorkflowScheduleFields({ autoFocus = false }: { autoFocus?: boolean }) { const { control, setValue, watch } = useFormContext() const scheduleEnabled = watch('scheduleEnabled') const scheduleCronExpression = watch('scheduleCronExpression') @@ -19,6 +23,7 @@ export function AgenticWorkflowScheduleFields() { return (
{ 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 1ffab23d300..ccd2d88b60c 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` }) From 3fe443bb8ff99bc16056c03a8e8899ccfb7a6ad2 Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 27 Aug 2026 15:42:29 +0200 Subject: [PATCH 3/3] fix(agent-task): group schedule with service information --- .../agentic-workflow-configuration.tsx | 16 +++--- .../agentic-workflow-context.tsx | 1 - .../agentic-workflow-schedule-fields.spec.tsx | 11 ++-- .../agentic-workflow-schedule-fields.tsx | 52 +++++++++++-------- .../agentic-workflow-summary.spec.tsx | 1 + .../agentic-workflow-summary.tsx | 3 -- 6 files changed, 43 insertions(+), 41 deletions(-) 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 06187dc1e2e..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 @@ -29,7 +29,6 @@ import { GitRepositoryCard } from './git-repository-card' const sectionOrder: AgenticWorkflowConfigurationSection[] = [ 'service-information', - 'schedule', 'ai-model', 'connectors', 'git-repositories', @@ -61,7 +60,6 @@ function isSectionCompleted( function getSectionTitle(section: AgenticWorkflowConfigurationSection) { const titles: Record = { 'service-information': 'Service information', - schedule: 'Schedule', 'ai-model': 'AI model', connectors: 'MCPs', 'git-repositories': 'Git repositories', @@ -238,8 +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(), - schedule: !isAgenticWorkflowScheduleValid(values), + 'service-information': !values.name.trim() || !isAgenticWorkflowScheduleValid(values), 'ai-model': !values.modelApiKey.trim() || Boolean(modelSettingsJsonError), connectors: Boolean(mcpJsonError), 'git-repositories': !gitRepositoriesValid, @@ -383,12 +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 })} /> - - - - - - + + { - it('focuses the schedule toggle when requested', () => { + it('links to the CRON expression builder when scheduling is enabled', () => { renderWithProviders( - wrapWithReactHookForm(, { + wrapWithReactHookForm(, { defaultValues: { - scheduleEnabled: false, + scheduleEnabled: true, scheduleCronExpression: '0 8 * * 1-5', timezone: 'Etc/UTC', }, }) ) - expect(screen.getByRole('switch', { name: 'Schedule agent task' })).toHaveFocus() + expect(screen.getByRole('link', { name: 'CRON expression builder' })).toHaveAttribute( + 'href', + 'https://crontab.guru/' + ) }) it('requires a valid cron expression only when scheduling is enabled', () => { 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 index ed7903aff08..7b20cdaebac 100644 --- 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 @@ -1,5 +1,5 @@ import { Controller, useFormContext } from 'react-hook-form' -import { InputText, InputToggle } from '@qovery/shared/ui' +import { ExternalLink, InputText, InputToggle } from '@qovery/shared/ui' import { formatCronExpression } from '@qovery/shared/util-js' import { TimezoneSetting } from '../../timezone-setting/timezone-setting' @@ -13,7 +13,7 @@ export function isAgenticWorkflowScheduleValid(values: AgenticWorkflowScheduleFo return !values.scheduleEnabled || Boolean(formatCronExpression(values.scheduleCronExpression)) } -export function AgenticWorkflowScheduleFields({ autoFocus = false }: { autoFocus?: boolean }) { +export function AgenticWorkflowScheduleFields() { const { control, setValue, watch } = useFormContext() const scheduleEnabled = watch('scheduleEnabled') const scheduleCronExpression = watch('scheduleCronExpression') @@ -23,7 +23,6 @@ export function AgenticWorkflowScheduleFields({ autoFocus = false }: { autoFocus return (
setValue('scheduleEnabled', value, { shouldDirty: true, shouldValidate: true })} /> {scheduleEnabled ? ( -
- Boolean(formatCronExpression(value)) || 'Invalid cron expression.', - }} - render={({ field, fieldState: { error } }) => ( - - )} - /> - +
+
+ + 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 24141a85f4e..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' }) }) 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 ccd2d88b60c..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 @@ -170,9 +170,6 @@ export function AgenticWorkflowSummary() { - - - handleEditSection('schedule')}> {values.scheduleEnabled ? ( <>