Argo CD’s sync policies are your secret weapon for controlling exactly when your applications get updated, preventing those "oops, I just pushed a breaking change to prod" moments.

Let’s see it in action. Imagine we have a simple Git repository with our Kubernetes manifests:

# ./apps/my-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-username/my-repo.git
    targetRevision: HEAD
    path: ./my-app
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

And within ./my-app/deployment.yaml in that repo:

# ./my-app/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: nginx
        image: nginx:1.21.6 # <-- We'll change this
        ports:
        - containerPort: 80

When Argo CD first creates this Application resource, it will immediately sync the my-app to the my-app namespace in our cluster. Now, let’s talk about controlling that sync.

The syncPolicy field is where the magic happens. The most common and powerful setting is automated. When automated is enabled, Argo CD will continuously watch your Git repository for changes and, if it detects any differences between your Git state and your cluster state, it will automatically initiate a sync.

Within automated, we have two crucial sub-fields: prune and selfHeal.

  • prune: true means that if a resource is removed from your Git repository, Argo CD will also delete it from your Kubernetes cluster during a sync. This keeps your cluster clean and free from orphaned resources.
  • selfHeal: true means that if someone manually changes a resource in your cluster to deviate from what’s defined in Git, Argo CD will automatically revert that change during the next sync. This enforces GitOps: Git is the single source of truth.

So, with the above configuration, any change to the files in the ./my-app directory of https://github.com/your-username/my-repo.git will trigger an automatic sync. If we update nginx:1.21.6 to nginx:1.22.0 in deployment.yaml and push it, Argo CD will detect the drift and update the deployment. If someone kubectl delete pod ... for a pod managed by this deployment, selfHeal would bring it back.

But what if you don’t want every change to sync immediately? This is where syncPolicy.syncOptions comes in. You can specify syncOptions to fine-tune the behavior.

Consider this:

# ./apps/my-app-manual-sync.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-manual-sync
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-username/my-repo.git
    targetRevision: HEAD
    path: ./my-app
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app-manual
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - SyncWave=1 # Example of a sync option, not directly controlling timing

The syncOptions are more about controlling the order of operations during a sync (e.g., using SyncWave to ensure a Deployment is ready before a Service is created). For controlling when a sync happens, we primarily leverage automated and its absence, or specific triggers.

The key is that if automated is not present in the syncPolicy, Argo CD will not automatically sync changes. The application will be marked as "OutOfSync" whenever Git and the cluster diverge, and you’ll need to manually trigger a sync via the Argo CD UI or argocd app sync my-app-manual-sync. This is perfect for environments where you want a human in the loop before deploying changes.

Another powerful feature for controlling sync timing is selective sync. You can configure your Application to only sync specific resources within a path. This is done via the syncPolicy.syncOptions with the ApplyOutOfSyncOnly flag, or more commonly, by defining spec.syncPolicy.syncOptions with CreateNamespace=true and then specifying spec.source.kustomize.patches or spec.source.helm.values to conditionally include/exclude resources. However, the most direct way to control sync is by controlling the automated block.

If automated is omitted entirely, syncs are manual. If automated is present but empty (automated: {}), it means automatic syncs are enabled, but without prune or selfHeal.

A common pattern to achieve "sync on commit, but only after approval" is to use a CI pipeline that triggers Argo CD. Your CI pipeline can build an image, update the manifest in Git, and then, after a manual approval step in the CI system, trigger an Argo CD sync using the argocd app sync command. This keeps the GitOps flow but adds an external gate.

The most surprising truth about Argo CD sync policies is that automated: {} (an empty automated block) effectively disables selfHeal and prune while still enabling automatic syncing of manifest changes. This is often overlooked, leading to unexpected behavior where Argo CD syncs changes but doesn’t clean up deleted resources or fix manual cluster modifications.

When you want to stage rollouts, you might have multiple Argo CD Application resources pointing to the same Git path but with different destination.namespace and syncPolicy settings. For example, a dev app that auto-syncs, a staging app that requires manual sync, and a prod app that also requires manual sync but with additional pre-sync hooks defined in syncPolicy.hooks.

The next concept you’ll want to explore is Argo CD’s ApplicationSets, which allow you to dynamically generate Argo CD Applications based on external data sources, enabling sophisticated multi-cluster and multi-environment deployments.

Want structured learning?

Take the full Argocd course →