Overriding a scheduled task scope
In this example, we’ll combine configuration and behavior overrides to build a scope for scheduled tasks applications that run on a schedule (via Cron), do their work, and exit.
Common uses include:
- Background jobs
- Report generation
- Event processors
- Any workload that doesn’t need to expose a network endpoint
We recommend first setting up the
production-ready scheduled task scope
to get a working baseline.
This guide then builds on that base implementation, overriding it with custom configuration and behavior.
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
└── ..
1. Skip DNS configuration
Scheduled jobs don’t need a service or DNS record. We can skip these steps in both the create
and delete
workflows.
Repo structure:
your-override-repo/
├── scope/ # Scope lifecycle actions
│ └── workflows/
│ ├── create.yaml
│ └── delete.yaml
└── ..
To do that, override the default create
and delete
workflows for the scope:
# 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
2. Use a CronJob
instead of a Deployment
In Kubernetes, scheduled jobs use CronJob
objects. We’ll replace the base Deployment template with a CronJob template.
Repo structure:
your-override-repo/
├── deployment/
│ └── templates/
│ └── deployment.yaml.tpl
└── values.yaml # Your configuration overrides
Inside the template (deployment.yaml.tpl
), add:
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"
3. Modify the deployment workflow
Because we’re no longer using Ingress
or Service
, we can skip the route traffic
step and replace the deployment
logic with one that applies our CronJob template.
Repo structure:
your-override-repo/
│ └── workflows/
│ └── initial.yaml
└── ..
In 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
This ensures the correct manifest is generated and applied, without unnecessary networking logic.
Final result
With these overrides, your scheduled task scope will:
- Skip DNS creation and deletion
- Use a Kubernetes
CronJob
instead of aDeployment
- Remove unnecessary networking steps
- Still manage secrets, resources, and logs
This is a minimal working example — you can add metrics, custom logging, or even a force-run action if needed.
You'll find the complete implementation of this scope in our repo: https://github.com/nullplatform/scopes/tree/main/cronjob.