feat(scheduledrun): add cron-based agent execution - #2097
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces a new v1alpha2 ScheduledRun capability to execute Agents/SandboxAgents on a cron schedule, spanning CRD + controller scheduler, REST API endpoints, UI creation/list/detail flows, and supporting metrics/RBAC updates.
Changes:
- Adds ScheduledRun CRD/types plus controller scheduler/controller logic and Prometheus metrics.
- Adds REST API surface for ScheduledRuns (list/get/create/update/delete/trigger) and UI pages/components to manage schedules and view run history.
- Updates RBAC and agent listing to support schedulable agent selection and ScheduledRun resource access.
Reviewed changes
Copilot reviewed 35 out of 37 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/types/index.ts | Adds frontend types for ScheduledRun CRD, status, and run history. |
| ui/src/lib/scheduledRuns.ts | UI helpers for rendering ScheduledRun target refs and display status. |
| ui/src/lib/formatDateTime.ts | Adds a shared datetime formatting helper for schedule/run history views. |
| ui/src/components/schedules/ScheduledRunList.tsx | New schedules list view with trigger/edit/delete actions. |
| ui/src/components/schedules/RunHistoryTable.tsx | New run-history table for ScheduledRun detail page. |
| ui/src/components/Header.tsx | Adds navigation entries for Scheduled Runs and “New Scheduled Run”. |
| ui/src/components/DeleteAgentButton.tsx | Adds toast feedback for agent deletion success/failure. |
| ui/src/app/schedules/page.tsx | Adds /schedules route entrypoint. |
| ui/src/app/schedules/new/page.tsx | Adds create/edit ScheduledRun form page. |
| ui/src/app/schedules/[namespace]/[name]/page.tsx | Adds ScheduledRun detail page with run history and trigger/suspend actions. |
| ui/src/app/actions/scheduledRuns.ts | Adds server actions to call ScheduledRun REST endpoints and revalidate UI paths. |
| ui/src/app/actions/agents.ts | Adds getSchedulableAgents() to list agents without AgentHarness rows. |
| helm/kagent/templates/rbac/writer-role.yaml | Grants write permissions for ScheduledRun resources/finalizers. |
| helm/kagent/templates/rbac/getter-role.yaml | Grants get/list/watch and status access for ScheduledRuns. |
| helm/kagent-crds/templates/kagent.dev_scheduledruns.yaml | Helm-templated ScheduledRun CRD manifest. |
| go/go.mod | Adds robfig/cron dependency for scheduler and API validation. |
| go/go.sum | Records robfig/cron module checksums. |
| go/core/test/e2e/scheduledrun_api_test.go | Adds end-to-end REST API lifecycle tests for ScheduledRuns. |
| go/core/pkg/app/app.go | Wires ScheduledRun scheduler/controller into manager and HTTP server. |
| go/core/internal/scheduledrun/target.go | Shared utilities for resolving/validating ScheduledRun targets. |
| go/core/internal/metrics/scheduledrun.go | Adds Prometheus metrics for dispatch/outcomes/durations/active schedules. |
| go/core/internal/httpserver/server.go | Adds ScheduledRuns routes and handler wiring in HTTP server. |
| go/core/internal/httpserver/handlers/test_helpers_test.go | Registers ScheduledRun types in handler test scheme. |
| go/core/internal/httpserver/handlers/scheduledruns.go | Implements ScheduledRuns REST handlers + schedule validation + trigger endpoint. |
| go/core/internal/httpserver/handlers/scheduledruns_test.go | Adds unit tests for ScheduledRuns handler behaviors. |
| go/core/internal/httpserver/handlers/handlers.go | Adds ScheduledRuns handler to handlers bundle (conditional on trigger availability). |
| go/core/internal/httpserver/handlers/agents.go | Adds query param to exclude AgentHarness rows from agent list responses. |
| go/core/internal/httpserver/handlers/agents_test.go | Adds tests for excluding AgentHarness and ScheduledRun interactions with deletions. |
| go/core/internal/controller/scheduledrun_scheduler.go | Implements cron scheduling, dispatch, run-history recording, and outcome polling. |
| go/core/internal/controller/scheduledrun_scheduler_test.go | Adds scheduler unit tests for scheduling and runOnce behavior. |
| go/core/internal/controller/scheduledrun_controller.go | Adds ScheduledRun controller reconcile logic (validation, nextRunTime, Accepted condition). |
| go/core/internal/controller/scheduledrun_controller_test.go | Adds controller unit tests for acceptance/rejection scenarios. |
| go/core/internal/a2a/agent_client_registry.go | Adds route-key based send method for A2A client registry lookups. |
| go/core/internal/a2a/a2a_handler_mux.go | Exposes helpers to compute route keys for Agent vs SandboxAgent. |
| go/api/v1alpha2/zz_generated.deepcopy.go | Updates generated deep-copy code for new ScheduledRun API types. |
| go/api/v1alpha2/scheduledrun_types.go | Adds ScheduledRun API types, validation markers, and constants. |
| go/api/config/crd/bases/kagent.dev_scheduledruns.yaml | Adds controller-gen base CRD for ScheduledRun. |
Files not reviewed (1)
- go/api/v1alpha2/zz_generated.deepcopy.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
f91aa90 to
71b93d8
Compare
9442180 to
a8fdf4b
Compare
EItanya
left a comment
There was a problem hiding this comment.
This PR is looking good overall, but there are definitely a few really important pieces that need looking at. I also haven't had a chance to dive deep on the cron logic yet, which I will do.
I will leave UI review here to @peterj
| // AgentRef is a reference to the Agent or SandboxAgent to execute. If | ||
| // Namespace is empty it defaults to the ScheduledRun's namespace. | ||
| // +required | ||
| AgentRef AgentReference `json:"agentRef"` |
There was a problem hiding this comment.
Can we use a TypedObjectReference here so that we can potentially support more in the future?
There was a problem hiding this comment.
Hi @EItanya,
Do you mean that we can have MCP crons in the future? Would love to change so
| // +optional | ||
| // +kubebuilder:validation:MaxLength=63 | ||
| // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` | ||
| Namespace string `json:"namespace,omitempty"` |
There was a problem hiding this comment.
Why do we accept Namespace if we don't actually allow cross namespace references? I think we should omit it until we decide we want it
There was a problem hiding this comment.
Hi @EItanya,
Do you think it valuable that we allow cross-namespace references?
There was a problem hiding this comment.
We now use TypedObjectReference. I think keeping the namespace semantics within the reference makes the API more extensible, but the question is whether we actually want to support it.
There was a problem hiding this comment.
What we can do it use a LocalObjectReference which means we can always add it later if we want
| @@ -68,7 +68,8 @@ func (h *AgentsHandler) handleListAgents(w ErrorResponseWriter, r *http.Request, | |||
| return | |||
| } | |||
|
|
|||
| agentsWithID, err := h.listAgentResponses(r.Context(), log, opts...) | |||
| includeAgentHarness := r.URL.Query().Get("includeAgentHarness") != "false" | |||
There was a problem hiding this comment.
If we are going to add filters to this page I would much rather do it generically than using oneoff filters like this. Can you omit this from this PR and do a separate one with that change?
|
Hi @EItanya, thanks for the dedicated review. |
2aeaa73 to
20bad21
Compare
|
Hi @EItanya, |
Hey there, sorry for taking so long, We had an offsite last week and then i had a couple days PTO, just getting back to it now |
EItanya
left a comment
There was a problem hiding this comment.
Things are looking great overall, I want to dig into the scheduler itself which I will do next :)
| // +optional | ||
| StartTime metav1.Time `json:"startTime"` |
| // DefaultScheduledRunMaxRunHistory is used when spec.maxRunHistory is omitted. | ||
| const DefaultScheduledRunMaxRunHistory = 10 |
There was a problem hiding this comment.
Do we also store the rest in the DB so they're available if the user specifically wants to check them for any reason?
| // are still allowed. | ||
| // +optional | ||
| // +kubebuilder:default=false | ||
| Suspend bool `json:"suspend,omitempty"` |
There was a problem hiding this comment.
Suspended? This describes a state
| if err := scheduledrun.ValidateTargetNamespaceAccess(ctx, r.Kube, sr.Namespace, target); err != nil { | ||
| if errors.Is(err, scheduledrun.ErrTargetAccessDenied) { | ||
| return ctrl.Result{}, r.rejectScheduledRun(ctx, &sr, scheduledRunReasonTargetReferenceNotAllowed, err.Error()) | ||
| } | ||
| return ctrl.Result{}, fmt.Errorf("failed to validate target namespace access: %w", err) | ||
| } |
There was a problem hiding this comment.
Same comment about using LocalObjectReference
| // policy to permit the ScheduledRun's namespace. | ||
| // +required | ||
| // +kubebuilder:validation:XValidation:rule="has(self.apiGroup) && self.apiGroup == 'kagent.dev'",message="targetRef.apiGroup must be kagent.dev" | ||
| TargetRef corev1.TypedObjectReference `json:"targetRef"` |
| t := true | ||
| resp.ReadOnly = &t | ||
| } | ||
| if scheduledRunReadOnly { |
There was a problem hiding this comment.
Maybe read-only should actually be defined on the ScheduledRun. Seems to me that a user could interact
There was a problem hiding this comment.
Read-only behavior is now defined by ScheduledRun.spec.allowSessionInteraction, defaulting to false. I think that's more extensible.
| } | ||
| normalizeScheduledRun(&sr) | ||
|
|
||
| if apiErr := h.validateScheduledRunObject(r, &sr); apiErr != nil { |
There was a problem hiding this comment.
I don't think we need to do this, it's really a premature optimization. We can reuse the logic from the controller if we really want, but I think it's also fine to just try and create and then surface errors from the object to the user, or let them check the object after it's been created. I'd rather start smaller
| } | ||
| existing.Spec = updatedSpec | ||
|
|
||
| if err := h.KubeClient.Update(r.Context(), existing); err != nil { |
There was a problem hiding this comment.
Any reason not to use server side apply here? This is also skipping metadata updates to things like labels or annotations
Hi, thanks for the review! |
260ff9c to
f9de17f
Compare
| getScheduledRun(name, namespace), | ||
| getScheduledRunExecutions(name, namespace, undefined, undefined, EXECUTIONS_PAGE_SIZE), | ||
| ]); | ||
| if (response.error || !response.data) { |
There was a problem hiding this comment.
do we need to check for errors from executionsResponse?
There was a problem hiding this comment.
Now it checks executionsResponse.error and surface it through the page error handling.
| const key = executionKey(execution); | ||
| if (!merged.has(key)) merged.set(key, execution); | ||
| } | ||
| return [...merged.values()].sort( |
There was a problem hiding this comment.
return the existing array if there we're no changes; otherwise you're always returning the new array here and re-triggering the useEffect on line 109
| }; | ||
|
|
||
| const handleDelete = async () => { | ||
| try { |
There was a problem hiding this comment.
set the isDeleting and show a deleting spinner in the dialog/disable the delete button while in progress
There was a problem hiding this comment.
The dialog now tracks isDeleting, disables both actions while deletion is in progress, and shows a spinner with “Deleting...”
|
|
||
| const isFormDisabled = state.isSubmitting || state.isLoading; | ||
|
|
||
| if (state.isSubmitting) { |
There was a problem hiding this comment.
you probably don't need this check here as you're using isFormDisabled in the form below to disable controls
That nil reference error should be resolved by #2363 |
Thanks @onematchfox ! |
f9de17f to
06bd48a
Compare
Signed-off-by: 0xLeo258 <noixe0312@gmail.com>
06bd48a to
7e6ad0d
Compare
Signed-off-by: JacksonWang <jackson.wang03@sap.com>

Adds the v1alpha2 ScheduledRun CRD, controller scheduler, REST API, metrics, and UI flow for creating, listing, viewing, updating, and manually triggering scheduled agent runs.
ScheduledRun targets resolve through a shared same-namespace Agent/SandboxAgent reference path. Suspended runs do not schedule next executions and cannot be manually triggered until resumed.
RunStatus unifies dispatch and outcome into a single enum (DispatchFailed/Pending/Succeeded/Failed/Timeout); RunHistoryEntry is StartTime, EndTime, SessionID, Status, Message. The outcome poller is restart-safe: Pending entries are resumed on Start so a pod restart between dispatch and terminal resolution does not leave entries stuck Pending.
CRD admission pattern annotations enforce DNS-label constraints, so duplicate handler-side DNS validators are removed. Service creation is enabled for agents.x-k8s.io Sandboxes so SandboxAgent A2A endpoints are reachable in real e2e deployments.
Includes CRD/Helm generation plus focused unit and e2e coverage.