Last reviewed: 2026-11-12
Working with Scheduled Jobs
Introduction
This guide shows how to run a scheduled job using the argo-cronworkflow-job chart. It is a thin wrapper around Argo Workflows’ CronWorkflow resource - the same idea as a Kubernetes CronJob, but built on Argo Workflows, which is what runs scheduled work on the platform.
This covers running a single container on a schedule, with environment variables, secrets, and AWS permissions - what most scheduled jobs need. Jobs with multiple steps are covered separately, under Advanced: multi-step jobs (DAG).
Table of contents
Quick start: a simple cron job
In your apps repository, add a folder for the job under the relevant namespace folder:
apps/team-dev/my-cron-job/
├── application.yaml
└── values.yaml
application.yaml tells ArgoCD which chart to deploy:
apiVersion: v2
name: my-cron-job
description: my-cron-job
version: 0.1.0
helm:
chart: helm/argo-cronworkflow-job
chartVersion: "1.3.0" # Example version. Check https://github.com/jppol-idp/helm-idp/tree/main/charts/argo-cronworkflow-job for the latest version
values.yaml defines the schedule and the container that runs on it - this is the minimum needed for a working job:
metadata:
jobName: "my-cron-job"
spec:
schedules:
- "0 23 * * *"
workflowSpec:
entrypoint: my-cron-job
templates:
- name: my-cron-job
container:
image: 354918371398.dkr.ecr.eu-west-1.amazonaws.com/my-team/my-cron-job:latest
command: ["node"]
args: ["runJob.js"]
Once committed, ArgoCD deploys a CronWorkflow that runs your container on the given schedule. Everything else - concurrency behavior, history limits, timezone - has a sensible chart default; add it only if you need something other than the default. See the reference table below.
Configuration reference
| Field | Default | Description |
|---|---|---|
metadata.jobName | example-cron-job | Name of the CronWorkflow resource. Required. |
spec.schedules | ["*/1 * * * *"] | One or more cron schedules the job runs on. |
spec.concurrencyPolicy | Replace | What happens if a run is still going when the next schedule fires: Replace stops the running job and starts the new one, Forbid skips the new run, Allow runs both concurrently. |
spec.successfulJobsHistoryLimit / spec.failedJobsHistoryLimit | 5 | How many past runs of each outcome to keep for inspection. |
spec.startingDeadlineSeconds | 30 | How much of a delay is tolerated before a missed schedule is abandoned rather than started late. |
spec.suspend | false | Set to true to pause the schedule without removing the job. |
spec.timezone | unset (UTC) | IANA timezone the schedules run in, e.g. Europe/Copenhagen. Daylight saving time is handled, so 0 6 * * * runs at 06:00 Danish time all year. |
env sets environment variables shared by every container template in the job - see Configuring your app with environment variables for the general pattern.
CPU and memory
Set resources on the container in each template, in the same format as a Kubernetes container (and idp-advanced):
spec:
workflowSpec:
templates:
- name: my-cron-job
container:
image: 354918371398.dkr.ecr.eu-west-1.amazonaws.com/my-team/my-cron-job:latest
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
memory: 1Gi
In a multi-step job, each step has its own resources.
Secrets
Use external_secrets to pull values from AWS Secrets Manager into the job’s environment, the same way other IDP charts do - see Working with Secrets for how secrets get into Secrets Manager in the first place.
external_secrets:
- env_name: API_CLIENT_SECRET
secretsmanager_name: my-cron-job_api-client-secret
env_name is the environment variable name inside the container. secretsmanager_name is the secret’s name under customer/<namespace>/ in Secrets Manager - the namespace and path prefix are added automatically. Add property if the secret holds a JSON object and you only want one key from it.
Several entries can read different keys from the same secret, e.g. the credentials secret created for an idp-postgresql database. This requires chart version 1.3.0 or later:
external_secrets:
- env_name: DB_USER
secretsmanager_name: my-db-write
property: username
- env_name: DB_PASSWORD
secretsmanager_name: my-db-write
property: password
IAM permissions with IRSA
If your job needs to call AWS APIs directly (S3, DynamoDB, SES, etc.), enable IAM Roles for Service Accounts (IRSA) on the job’s ServiceAccount and attach the permissions it needs. Requires chart version 1.2.6 or later.
serviceAccount:
create: true
irsa:
enabled: true
iamPolicyStatements:
- Effect: Allow
Action:
- s3:ListBucket
Resource:
- arn:aws:s3:::my-team-artifacts
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource:
- arn:aws:s3:::my-team-artifacts/*
This creates an IAM role trusted by the job’s ServiceAccount, with the policy statements above attached. For S3 access specifically, see Workload access (IRSA) in the S3 guide - the same iamPolicyStatements pattern applies there.
Testing locally
To run or debug your job’s code locally with the same AWS permissions it has in the cluster, assume the job’s IRSA role from your workstation:
eval "$(./scripts/assume-<namespace>-irsa-role.sh <job-name>)"
aws sts get-caller-identity
<job-name> is metadata.jobName from your values.yaml, and the scripts live in your team’s apps repository under scripts/ - see Assuming the S3 access role in the S3 guide for the equivalent flow against the namespace’s S3 access role.
Auto-updating the image
To have ArgoCD Image Updater bump the job’s image tag automatically when a new build lands in ECR, see Working with Argo Workflows and other non-standard CRDs in the auto-updates guide - it walks through the exact argocd_image_updater_helm_image_spec and argocd_image_updater_force_update settings this chart needs.
Advanced: multi-step jobs (DAG)
Everything above covers a single container on a schedule, which is what most jobs need. If a job needs multiple steps in one run - in order, in parallel, or with dependencies between them - workflowSpec.templates can hold several templates wired together with a Directed Acyclic Graph (DAG) instead of just one. Requires chart version 1.2.6 or later.
Give one template a dag block instead of a container block, and reference your other templates by name:
spec:
schedules:
- "0 23 * * *"
workflowSpec:
entrypoint: my-cron-job
templates:
- name: my-cron-job
dag:
tasks:
- name: fetch
template: fetch-data
- name: process
template: process-data
dependencies:
- fetch
- name: fetch-data
container:
image: 354918371398.dkr.ecr.eu-west-1.amazonaws.com/my-team/my-cron-job:latest
command: ["node"]
args: ["fetch.js"]
- name: process-data
container:
image: 354918371398.dkr.ecr.eu-west-1.amazonaws.com/my-team/my-cron-job:latest
command: ["node"]
args: ["process.js"]
entrypoint points at the DAG template (my-cron-job here), not at one of the container steps. Each task’s template field names one of the other templates in the list; dependencies controls ordering - tasks with no dependency on each other run in parallel. See Argo Workflows’ DAG documentation for the full set of options (loops, conditionals, passing artifacts between steps).
Everything else on this page - secrets, IRSA, auto-updating - applies the same way whether a job has one template or several.
Running a job manually
To start a run right away without changing the schedule, use the Argo Workflows UI at https://argo-workflows.<cluster>.idp.jppol.dk, where <cluster> is the cluster your namespace runs on. Log in with SSO, go to Cron Workflows, pick your namespace and job, and click Submit. To rerun an earlier run, open it under Workflows and click Resubmit.
Access uses the same Argo group that gives you access to your team’s applications in ArgoCD.
Monitoring & troubleshooting
Each run shows up as a Kubernetes Job/Workflow in the namespace, and its logs are collected the same way as any other workload - see Application logs. To get notified when a run fails, see Alerting.
Common gotchas:
- Nothing seems to run, or
cowsayoutput shows up somewhere unexpected. The chart’s defaults deploy a working example job (whalesay, printing to logs viacowsay) so it’s usable out of the box - ifspec.workflowSpecisn’t fully overridden in yourvalues.yaml, parts of that default may still be in effect. Diff your renderedCronWorkflowagainst what you expect if a run doesn’t do what you think it should. - A run doesn’t fire when expected, or seems to replace one still in progress. Check
spec.concurrencyPolicy- the default,Replace, kills an in-flight run when the next schedule comes due rather than letting both run or skipping the new one. Set it toForbidif overlapping runs would cause problems (e.g. two runs writing to the same resource). - A scheduled run never starts at all. If the workflow controller was down or delayed past
spec.startingDeadlineSecondswhen a schedule fired, that run is skipped rather than started late. RaisestartingDeadlineSecondsif late-but-still-useful runs matter for your job. AccessDeniedcalling AWS APIs from inside the job. CheckserviceAccount.irsa.enabledistrueand thatiamPolicyStatementsactually covers the action and resource being called - see IAM permissions with IRSA.AccessDeniedonsts:AssumeRolewhen assuming the job’s role from your own machine. Requires chart version1.2.6or later - earlier versions only trust the running pod’s identity, not a human session.secret "<job>-env-<name>" not foundwhen the job starts. If severalexternal_secretsentries share the samesecretsmanager_name, chart versions before1.3.0give them the same name, so only the last one is created. Upgrade to1.3.0or later - see Secrets.- A DAG template fails validation, or a step runs with the wrong container. Requires chart version
1.2.6or later for multi-step jobs - earlier versions could inject a stray container into DAG parent templates.
If something here doesn’t cover your case, ask in your onboarding Slack channel - the IDP team is happy to help.