Skip to main content

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:

  1. Replaces the Kubernetes Deployment with a CronJob.
  2. Skips DNS, Service, and Ingress provisioning (no network endpoint needed).
  3. 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:

  1. Install the nullplatform agent in your cluster using Helm.
  2. Clone the scopes repository and set SERVICE_PATH=scheduled_task.
  3. Configure your environment variables (API key, NRN, environment).
  4. Run the ./configure script to register the scope schema, actions, and notification channel.
  5. 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 CronJob instead of a Deployment
  • Remove unnecessary networking steps
  • Still manage secrets, resources, and logs

You can find the complete implementation in the scopes repository.

Next steps