Scheduled tasks scope
The Scheduled tasks scope runs periodic jobs on Kubernetes using CronJobs. Use it for any workload that runs on a schedule, does its work, and exits without exposing a network endpoint.
When to use it
Use the Scheduled tasks scope when your workload runs on a recurring schedule, doesn't serve HTTP traffic, and performs a finite task before exiting, such as ETL pipelines, report generation, data cleanup, or batch event processing.
What it includes
The Scheduled tasks scope provides built-in support for:
- Cron scheduling and concurrency control: define when jobs run using cron expressions, and control whether concurrent runs are allowed, replaced, or queued
- Retries and job history: set retry attempts for failed jobs and configure how many runs to keep
- Resource limits and logging: define CPU and memory per job, with logs streamed through the same pipeline as other scopes
How it works
Scheduled tasks are built as an override of the Containers scope. Instead of creating a long-running Deployment with networking, the scope:
- Replaces the Kubernetes Deployment with a CronJob.
- Skips DNS, Service, and Ingress provisioning (no network endpoint needed).
- Manages secrets, resources, and logs just like a Containers scope.
Get started
To set up a Scheduled tasks scope, you need a Kubernetes cluster, Helm, Gomplate, the nullplatform CLI, and an API key with agent roles.
The setup process follows these steps:
- Install the nullplatform agent in your cluster using Helm.
- Clone the scopes repository and set
SERVICE_PATH=scheduled_task. - Configure your environment variables (API key, NRN, environment).
- Run the
./configurescript to register the scope schema, actions, and notification channel. - Create your first Scheduled tasks scope from the UI.
The Scheduled tasks tutorial provides the full walkthrough with commands and checkpoints.
How the overrides work
The Scheduled tasks scope customizes the base Containers scope through configuration and behavior overrides. This section explains what each override does, so you can understand the implementation or further customize it for your needs.
Repo structure
Place workflow overrides under scope/workflows/ or deployment/workflows/ depending on the lifecycle action you're customizing.
your-override-repo/
├── scope/ # Scope lifecycle actions
│ └── workflows/
│ ├── create.yaml
│ ├── update.yaml
│ └── delete.yaml
├── deployment/
│ └── templates/
│ ├── deployment.yaml.tpl
│ └── workflows/
│ ├── initial.yaml
│ ├── blue_green.yaml
├── values.yaml # Your configuration overrides
└── ..
Skip DNS configuration
Scheduled jobs don't need a service or DNS record. The create and delete workflows skip the networking step:
# path: scope/workflows/create.yaml
include:
- "$SERVICE_PATH/values.yaml"
steps:
- name: networking
action: skip
# path: scope/workflows/delete.yaml
include:
- "$SERVICE_PATH/values.yaml"
steps:
- name: networking
action: skip
Use a CronJob instead of a Deployment
In Kubernetes, scheduled jobs use CronJob objects. The override replaces the base Deployment template with a CronJob template in deployment/templates/deployment.yaml.tpl:
apiVersion: batch/v1
kind: CronJob
metadata:
name: job-{{ .scope.id }}-{{ .deployment.id }}
namespace: {{ .k8s_namespace }}
labels:
name: d-{{ .scope.id }}-{{ .deployment.id }}
app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }}
nullplatform: "true"
spec:
schedule: "{{ .scope.capabilities.cron }}"
concurrencyPolicy: {{ .scope.capabilities.concurrency_policy }}
successfulJobsHistoryLimit: {{ .scope.capabilities.history_limit }}
failedJobsHistoryLimit: {{ .scope.capabilities.history_limit }}
jobTemplate:
metadata:
labels:
name: d-{{ .scope.id }}-{{ .deployment.id }}
app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }}
nullplatform: "true"
spec:
backoffLimit: {{ .scope.capabilities.retries }}
template:
metadata:
labels:
name: d-{{ .scope.id }}-{{ .deployment.id }}
app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }}
nullplatform: "true"
annotations:
nullplatform.logs.cloudwatch: 'true'
nullplatform.logs.cloudwatch.log_group_name: {{ .namespace.slug }}.{{ .application.slug }}
nullplatform.logs.cloudwatch.log_stream_log_retention_days: '7'
nullplatform.logs.cloudwatch.log_stream_name_pattern: >-
type=${type};application={{ .application.id }};scope={{ .scope.id }};deploy={{ .deployment.id }};instance=${instance};container=${container}
nullplatform.logs.cloudwatch.region: us-east-1
spec:
restartPolicy: OnFailure
securityContext:
runAsUser: 0
containers:
- name: application
image: {{ .asset.url }}
envFrom:
- secretRef:
name: s-{{ .scope.id }}-d-{{ .deployment.id }}
resources:
limits:
cpu: {{ .scope.capabilities.cpu_millicores }}m
memory: {{ .scope.capabilities.ram_memory }}Mi
requests:
cpu: {{ .scope.capabilities.cpu_millicores }}m
memory: {{ .scope.capabilities.ram_memory }}Mi
Then, point your values.yaml to this new template:
configuration:
DEPLOYMENT_TEMPLATE: "$OVERRIDES_PATH/deployment/templates/deployment.yaml.tpl"
Modify the deployment workflow
Because the scope no longer uses Ingress or Service, the deployment workflow skips the route traffic step and replaces the deployment logic with one that applies the CronJob template.
In deployment/workflows/initial.yaml:
include:
- "$SERVICE_PATH/values.yaml"
steps:
- name: route traffic
action: skip
- name: create deployment
type: script
action: replace
file: "$OVERRIDES_PATH/deployment/build_deployment"
output:
- name: DEPLOYMENT_PATH
type: file
file: "$OUTPUT_DIR/deployment-$SCOPE_ID-$DEPLOYMENT_ID.yaml"
- name: SECRET_PATH
type: file
file: "$OUTPUT_DIR/secret-$SCOPE_ID-$DEPLOYMENT_ID.yaml"
- name: apply
type: script
file: "$SERVICE_PATH/apply_templates"
configuration:
ACTION: apply
DRY_RUN: false
post:
name: wait deployment active
action: skip
Refresh the agent sources
After changing your override repo, refresh the agent sources so it pulls the latest code.
Result
With these overrides, your scheduled task scope will:
- Skip DNS creation and deletion
- Use a Kubernetes
CronJobinstead of aDeployment - Remove unnecessary networking steps
- Still manage secrets, resources, and logs
You can find the complete implementation in the scopes repository.
Next steps
- Override workflows: customize scope behavior for your organization
- Containers: the default scope type for long-running workloads
- Scope types: compare all available scope types