An AI agent types bex migrate into a chat window. Under the hood, that's a Kubernetes Job — a container that runs a database migration to completion, then reports back success or failure. Simple, until you ask: whose job is it to decide when that migration is actually done? Before Kubernetes 1.35, the honest answer was "whoever wins the race" — the built-in Job controller and any platform-specific logic layered on top were both writing to the same .status fields, and nothing stopped them from stepping on each other. As of 1.35, there's a field that ends the race outright: spec.managedBy, GA as of the December 2025 release, lets a platform's own controller take exclusive ownership of a Job's status while the stock controller steps aside entirely.
What managedBy Actually Does
The mechanism is a single immutable field on the Job spec. Leave it unset, or set it to the default kubernetes.io/job-controller, and Kubernetes behaves exactly like it always has. Set it to anything else, and the built-in Job controller skips that object completely — no pod creation, no status writes, no reconciliation of any kind:
apiVersion: batch/v1
kind: Job
metadata:
name: migrate-acme-prod
spec:
managedBy: batch.bex.co/agent-controller
template:
spec:
containers:
- name: migrate
image: registry.bex.co/acme/app:pr-482
command: ["./manage.py", "migrate"]
restartPolicy: NeverThat one line — managedBy: batch.bex.co/agent-controller — is the whole feature. Once set, it can't be changed (the field is immutable specifically to prevent orphaned pods from a mid-flight controller handoff), and from that point the external controller named there owns the object end-to-end: creating pods, tracking their lifecycle, and writing every field under .status.
The field shipped through the normal KEP maturity path — KEP-4368, alpha in 1.33, beta in 1.34, GA and enabled by default in 1.35. Its original motivation was MultiKueue, Kueue's multi-cluster dispatcher: a user creates a Job on a central management cluster, MultiKueue mirrors it onto whichever worker cluster has capacity, and status flows back from the worker's real execution to the management cluster's placeholder. Without managedBy, the management cluster's own Job controller would try to run that placeholder itself — creating pods nobody wanted, on a cluster that was never meant to execute the job at all.
The Pre-1.35 Workarounds — and Why They Don't Fit a Multi-Tenant PaaS
Before GA, teams that needed this same "hands off, I've got it" guarantee had exactly two blunt options, and KEP-4368's own motivation section points at both.
Option one: disable the Job controller cluster-wide. Run kube-controller-manager with --controllers=-job and the built-in controller stops reconciling every Job on the cluster, not just the ones you wanted to own yourself. For a single-tenant batch cluster that's tolerable. For a multi-tenant PaaS where most Jobs are ordinary tenant workloads that should behave exactly like stock Kubernetes, it's a non-starter — you'd be reimplementing the entire Job controller, correctly, for every tenant, just to get exclusive control over the small slice of Jobs your own platform creates.
Option two: wrap Jobs in a custom CRD. Define your own PlatformJob resource, have your controller create bare Pods directly, and skip the Job API entirely. This sidesteps the ownership fight because there's no Job object for the stock controller to see — but it also means forfeiting everything the Job API gives you for free: kubectl get jobs shows nothing, backoffLimit and completions and parallelism all have to be hand-rolled, and every tenant-facing tool or dashboard that expects to kubectl describe job a workload has to be taught about your bespoke resource instead.
managedBy replaces both with a scalpel: one field, on one Job, delegates that Job specifically — every other Job on the same cluster, tenant workloads included, keeps using the stock controller untouched.
What This Buys Agent-Triggered Batch Work
Here's where the migration-from-chat example earns its keep. On a deploy-from-chat platform, an AI agent doesn't run kubectl apply directly — it calls an MCP tool (run_migration, run_one_off_task) that the platform exposes, and the platform's reconciler is what turns that tool call into a Job. With managedBy set to the platform's own controller name, that reconciler now owns the entire lifecycle instead of fighting the stock controller for it:
| Responsibility | Stock Job controller | Platform's managedBy controller |
|---|---|---|
| Pod creation & retries | backoffLimit (fixed integer, no context) | Retry policy aware of why it's retrying — a flaky network dependency vs. a broken migration script get different backoff curves |
| Timeout | activeDeadlineSeconds (static) | Deadline set from the actual deploy context — a schema migration on a 50M-row table gets more time than a seed script |
| Progress visibility | None — you poll .status | Pod logs streamed live into the agent's chat transcript as the migration runs, not just a final pass/fail |
| Completion signal | .status.conditions[].type == Complete | Same field, but the write also triggers the platform's own "tell the agent it's done" webhook |
That table is only actionable because KEP-4368 also standardized what "own the status" means — a managedBy controller isn't free to write whatever it wants to .status. The API server now validates the same state machine the stock controller follows: Complete=True and Failed=True are mutually exclusive and can only land once .status.terminating and .status.ready both hit zero (no declaring victory while a pod is still shutting down), each terminal condition must be preceded by its interim condition (SuccessCriteriaMet or FailureTarget), and .status.completionTime is immutable once set. A platform controller that gets this wrong doesn't silently corrupt state — the write gets rejected. That's the real gap managedBy closes for agent-triggered work: not "can I claim this Job," which the annotation-based hacks half-solved already, but "can I claim it and have the platform guarantee I can't leave it in a state a human — or another agent — reading .status later would misinterpret."
Migrations are the sharpest example because they're irreversible and time-boxed, but the same delegation covers every other one-off an agent can trigger through the platform's MCP surface: a scheduled task an agent sets up once and never revisits, a background build kicked off mid-conversation so the agent can keep talking while it compiles, a data-export job a customer asks for by name. All of them are Jobs under the hood, and all of them benefit from the same thing a migration does — one controller, named explicitly on the object, that's unambiguously on the hook for telling the agent (and the human reading the transcript afterward) what actually happened.
The Ecosystem Is Already Building On It
A platform team weighing whether to hand-roll a managedBy controller or lean on something existing has real options as of 1.35. JobSet coordinates groups of related Jobs (e.g., a distributed training run's leader and workers) as one unit. Kubeflow Trainer and KubeRay do the same for their respective ML-training and Ray-cluster workloads. AppWrapper groups heterogeneous batch resources — Jobs, PodGroups, custom resources — under a single admission and scheduling decision. Tekton Pipelines uses it to keep CI/CD pipeline runs from fighting the stock controller over status while Tekton's own reconciler drives the pipeline forward.
None of them target "AI agent triggers a one-off migration from chat" directly — they're built for ML training, multi-cluster batch dispatch, and CI pipelines — but the pattern they establish is exactly the shape a platform's own agent-ops controller should copy: a purpose-built controller claiming a narrow, well-defined class of Jobs, leaving everything else untouched, and reusing the same status-validation guarantees the API server already enforces rather than inventing a parallel one. A platform doesn't need to depend on any of these five projects to benefit from managedBy — the value is in the primitive itself being GA and stable, not in which project popularized it first.
The Limits: Immutable, and You Still Own Full Conformance
Two caveats keep this from being a free lunch. First, the immutability that prevents orphaned pods also means there's no changing your mind mid-flight — a Job created under the stock controller can't be handed off to a custom one later, and vice versa; the delegation decision has to be made at creation time, which for an agent-triggered Job is fine (the platform's reconciler is the one creating it) but rules out retrofitting managedBy onto Jobs already running. Second, "the API server validates your status writes" is a floor, not a ceiling — it stops you from writing an invalid state machine, but the actual retry logic, timeout policy, and log-streaming in the table above are still code the platform has to write and operate. managedBy buys exclusive ownership of the Job object; it doesn't buy the controller that uses that ownership well.
For a Cluster API-based platform already running its own reconcilers for App and node lifecycle, that's a small, well-scoped addition — one more controller, following a pattern Kubernetes itself now standardizes, instead of one more annotation convention nobody outside the team remembers the rules for.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with agent-triggered one-off tasks (migrations, background jobs) reconciled by the platform's own Kubernetes controller instead of a fragile annotation hack. Star the repo on GitHub or deploy your first app today.