Introducing AI agent: Get information about your infrastructure faster. Learn more >

Change in behavior: Classes promises can reset the timer for currently defined persistent classes

Posted by Nick Anderson
June 16, 2026

CFEngine is changing how the persistence timer of a class defined by a classes promise is handled when that class is rediscovered on a later run. We consider this a bug fix: classes promises should always have been able to reset the timer, just like classes bodies can. A new timer_policy attribute on the classes promise type lets you choose whether the timer is reset on each evaluation (reset) or left to count down from its original expiry (absolute). In CFEngine 3.28.0 the default becomes reset, aligning classes promises with classes bodies. The attribute is also being backported to the 3.27.x and 3.24.x LTS streams (3.27.2 and 3.24.5), where the default stays absolute so that existing behavior is unchanged and the new behavior is strictly opt-in.

Persistent classes and their timers

A persistent class survives between agent runs for a set number of minutes, recorded in a local database (cf_state.lmdb). It is pre-defined before policy evaluation begins, so it can influence behavior very early in a run.

There are two ways to define a persistent class, and historically they did not behave the same way when the class was rediscovered on a later run:

  • The classes promise type, using the persistence attribute (minutes).
  • A classes body attached to another promise (for example if_repaired), using persist_time together with timer_policy.

The classes body has carried a timer_policy attribute for a long time, defaulting to reset: each time the owning promise repairs, the timer starts over. The classes promise type had no such control – it only had persistence, and the timer always behaved as absolute. That inconsistency is the bug this change addresses (tracked as CFE-4681).

The previous behavior

Previously, a classes promise whose class was already defined (loaded from the persistent store) was simply skipped. The timer counted down from the original expiry and the class disappeared after the configured minutes, regardless of how many agent runs happened in between. There was no way to extend the window directly (only as the result of some other promise using a classes body).

The new behavior: timer_policy

The classes promise type now accepts a timer_policy attribute:

  • absolute – preserve the original expiry (the old behavior).
  • reset – refresh the timer on each evaluation, even when the class is already defined.

With reset, the effect is a sliding window: the class stays defined as long as the expression keeps evaluating true, expiring after the configured minutes from the last run rather than the first.

Try it – run this policy twice:

body common control
{
  bundlesequence => { "demo" };
}

bundle agent demo
{
  classes:
    "my_class"
      expression => "any",
      persistence => "120",
      timer_policy => "reset";

  reports:
    "my_class is defined";
}
# Requires a cf-agent build that includes CFE-4681.
# Clear any prior state so the first run is a clean "Creating".
rm -f /var/cfengine/state/cf_state.lmdb*

echo "=== Run 1 ==="
cf-agent -Kvf /tmp/timer_policy_demo.cf 2>&1 | grep -i "persistent class"

echo "=== Run 2 ==="
cf-agent -Kvf /tmp/timer_policy_demo.cf 2>&1 | grep -i "persistent class"

Run 1 logs Creating persistent class ... policy reset and run 2 logs Resetting persistent class ... timer to 120 minutes.

Defaults and availability

The timer_policy attribute for classes promises arrives in three releases, but the default differs:

Release timer_policy available Default for classes promises
3.28.0 yes reset
3.27.2 yes (backport) absolute
3.24.5 yes (backport) absolute

In 3.28.0 the default flips to reset to align classes promises with classes bodies and match what most users expect: a persistent class that keeps being discovered should keep persisting. The backports ship the attribute on the LTS streams while keeping the absolute default so nothing changes unless you ask for it.

If you depend on a specific behavior, set timer_policy explicitly rather than relying on the default. That makes the policy behave the same on 3.24.5, 3.27.2, and 3.28.0 alike.

If you have questions or need help, reach out on the mailing list or GitHub discussions. If you have a support contract, feel free to open a ticket in our support system.