Automation and Cranks

Documentation snapshot 18 October 2026. Deployment · Audit Status
On this page

Permissionless calls advance series and other scheduled work when their checks pass. Automation depends on callers, gas, price availability and the configured limits; no operator is required to act at a promised time.

Design

fact Note Systems has no keeper daemon, cron job or privileged bot. Every state transition of a series is reachable in one of three ways, all of which are on-chain and permissionless:

fact Design
LayerWho triggers itWhat it doesReward
Explicit cranksAnyonefinalizeStrike, observe, settle, sweepFees, RollPolicy.roll, DeskPolicy.fillkeeperRewardQuote / RollPolicy.bounty / DeskPolicy.bounty
Lazy executionWhoever is already interactingclaim, redeem, redeemShield, settle and refund first run any due strike or observationkeeperRewardQuote to msg.sender
Chainlink-compatible upkeepAny automation network or botNoteAutomation.checkUpkeep / performUpkeep batch the aboverewards forwarded to msg.sender

If nobody cranks, the next user who touches the series does; the price observed is the same either way, because OracleAdapter selects the closing print deterministically (see Oracle and MarketCalendar).

Permissionless Cranks

fact None of the following require a role.

fact Permissionless Cranks Reference
FunctionWhenReward
NoteCore.finalizeStrike(seriesId)block.timestamp >= observations[0] and status SubscriptionkeeperRewardQuote (default 2 USDG) on success
NoteCore.observe(seriesId)block.timestamp >= observations[nextObs] and status LivekeeperRewardQuote on success
NoteCore.settle(seriesId, account)After strike, for any account with an unsettled depositnone (but may trigger a lazy crank, see below)
NoteCore.sweepFees(seriesId)Any time sweepableFees(seriesId) > 0, i.e. accruedFees above the keeper reserve (NoFees otherwise)none
RollPolicy.roll(underlying)Template enabled, feed fresh, cooldown elapsed, core accepting series, no series yet for today's trading-day close, fewer than maxOpenSeries later closes openmin(bounty, RollPolicy USDG balance)
DeskPolicy.fill(seriesId)Series in Subscription, inside the fill window (last fillLead before subscriptionEnd), inside the published rule for its underlying, S > D, cooldown elapsed, Desk has room and the sized fill is at least minFillmin(bounty, DeskPolicy USDG balance)
Minter.mintWeekly()Once per epoch week; each gauge is minted and notified in isolation, a failing gauge is skipped (GaugeSkipped) and its share is carried forward for a later permissionless Minter.retryGauge(gauge)none
Desk.harvest(seriesId)After a coupon or settlement; also queued by NoteAutomation once the series has settlednone directly; one keeperRewardCap via NoteAutomation when the harvest releases the position
RevenueRouter.sellNoteForQuoteAny time (the buyback auction)the auction price itself

The strike/observation reward is paid from the series’ accruedFees and capped by them: min(keeperRewardQuote, accruedFees). setKeeperReward is bounded by maxKeeperReward() (MAX_KEEPER_REWARD_UNITS = 100 quote units, 100 USDG). sweepFees leaves keeperReserve(seriesId) behind, one keeper reward per crank still due on the series (every close during Subscription, the closes from nextObs while Live, nothing once terminal), so an early sweep cannot remove the rewards of the remaining cranks; the reserve becomes sweepable when the series reaches a terminal status.

The sink is paid first and then notified; a failing notifyFee emits FeeNotifyFailed(seriesId, feeSink, amount) and does not block the sweep. A Deferred observation pays nothing until it succeeds. The reward is credited to keeperOwed[keeper] and withdrawn with NoteCore.claimKeeperReward(); NoteAutomation.performUpkeep claims it before forwarding, and a direct cranker claims it in a separate call. This keeps a keeper address that rejects transfers from blocking an observation.

Timing

Observation timestamps are official US closes, so every crank time is known in advance. observe reverts with TooEarly(closeTs) before the timestamp and with InvalidStatus if the series is not Live. The OracleAdapter rejects observe (GraceNotElapsed) until closeTs + closeGrace (5 min) has passed, then selects the last round published no later than that instant, whoever calls and whenever.

Cranking at OracleAdapter.resolvableAt(closeTs) or shortly after is ideal; calling later changes nothing except the reward race, unless more than maxRoundWalk rounds have been published since the close.

Reading the Queue

uint256 n = core.seriesCount();
for (uint256 id = 0; id < n; id++) {
    (bool strikeDue, bool observationDue, ) = core.dueWork(id);
    if (strikeDue) core.finalizeStrike(id);
    else if (observationDue) core.observe(id);
}

dueWork(seriesId) -> (strikeDue, observeDue, settleable) is readiness-based: it returns true only when the corresponding crank would succeed right now (close reached, oracle grace elapsed, oracle not paused, observation not deferred-and-pending). It returns (false, false, settleable) while NoteCore is paused.

observationDue(seriesId) -> bool is the cheaper calendar-only companion for leg listings: it is true while the series is Live, the next observation is unprocessed and the current time is past OracleAdapter.resolvableAt(effectiveObservationTs(seriesId, nextObs)) (the effective close plus the close grace). It reads no feed and ignores deferral or pause state, so it tells a marketplace or a wallet that a coupon event is pending on the leg it is about to trade, not whether observe will succeed this second.

Lazy Execution

fact claim, redeem, redeemShield, settle and refund call an internal _crankIfDue first. It finalises a due strike or processes up to MAX_LAZY_OBSERVATIONS = 4 due observations, paying keeperRewardQuote per successful step to msg.sender exactly as an explicit crank would, then continues with the user’s action against the updated state.

  • A lazy crank never makes the user’s action revert: if the strike/observation is not ready (grace not elapsed, oracle paused, observation deferred) it is simply skipped.
  • If the user’s action itself reverts (for example NothingToClaim), the whole transaction reverts, including the crank. Nothing is left half-done.
  • The result is byte-identical to the explicit path: test/fuzz/core/LazyCrank.fuzz.t.sol drives a lazy and an explicit twin series through random price paths and asserts equal balances, buckets and statuses.
  • Users who are not interested in cranking pay the extra gas only when work is actually due; dueWork lets front-ends warn about it.

RollPolicy

fact RollPolicy is the only address besides the owner allowed to call NoteCore.createSeries (NoteCore.seriesCreator, set by governance via setSeriesCreator). It holds one template per underlying and turns it into a concrete series on demand:

fact RollPolicy Reference
Template fieldMeaning
feedChainlink proxy used as the series feed
autocallBps, barrierBps, couponFloorBps, couponCapBps, refBpsSeries economics, validated against NoteCore limits
notionalCap, minTicketSubscription limits
subscriptionTradingDaysSubscription ends at the official close of the N-th trading day after today
strikeLagTradingDaysThe strike is the official close N trading days after the subscription close
observationCount, observationSpacingTradingDaysObservations are official closes spaced N trading days apart (weekends, holidays and early closes come from MarketCalendar)
maxOpenSeriesHow many series of the stock may have a deposit close later than today's (1 to 10); a roll is refused with OpenCap beyond it. The series closing today does not count, so maxOpenSeries equal to subscriptionTradingDays gives one new series per trading day
maxFeedAgeroll refuses a feed whose latest round is older than this
enabledGuardian can disable an underlying instantly

roll(underlying) (nonReentrant, whenNotPaused) opens the series of the current trading day.

Gates in order: TemplateDisabled, FeedStale (the feed’s latest print, read through a guarded call that treats a revert or malformed return data as no data, is stale, incomplete or invalid, and the oracle adapter’s reference close is also older than maxFeedAge), RollCooldown (minRollInterval, default 1 hour, max 30 days), NotSeriesCreator (the policy is no longer NoteCore.seriesCreator), CorePaused, CoreDeprecated (the core is no longer the registry’s active deployment), TemplateExceedsCoreLimits (the stored couponCapBps or observationCount is above the core’s current maxCouponCapBps or maxObservations, which governance can lower after the template was set), CalendarGap (the calendar has no official close within 14 days at some step of the schedule walk), AlreadyRolled (a series closing at the derived subscription close already exists: a second roll on the same trading day, or a roll on a weekend or holiday, which derives the same close as the last trading day before it) and OpenCap (maxOpenSeries series of that stock already have a close later than the end of today, UTC; the series on its last session does not count, whatever the calendar says about today after it was created).

It computes the dates from the core’s calendar (subscription close subscriptionTradingDays trading days ahead, strike strikeLagTradingDays trading days after the close, observations every observationSpacingTradingDays trading days), calls createSeries, emits Rolled(underlying, seriesId, caller, bountyPaid) and pays min(bounty, balance) USDG to the caller.

Live and settled series never block a roll; the cadence is maxOpenSeries against subscriptionTradingDays (equal values give one new series per stock per trading day, maxOpenSeries 1 gives one per subscription period, the next opening on the closing day of the current one, which is the testnet setting since 14 Oct 2026), and either way depositors find an open series on any trading day. rollAll() runs the same gates for every stock with a template, opens each series that is due, skips the rest without reverting and pays one bounty per series opened; rollRange(start, count) does the same for a window of the template list and reverts InvalidRange when the window is empty or runs past it. previewRoll(underlying) returns (params, canRoll, reason) with reason one of Paused, TemplateDisabled, FeedStale, RollCooldown, NotSeriesCreator, CorePaused, CoreDeprecated, CoreLimits, CalendarGap, AlreadyRolled, OpenCap.

Every gate createSeries applies is mirrored in the preview, so canRoll == true means the roll succeeds on the core in the same block; params is zeroed whenever canRoll is false, and the view does not revert on a stale or broken feed or on a calendar gap, so bots can simulate before sending. openSeriesOf(underlying), openCount(underlying), capCount(underlying), seriesOf(underlying) and calendar() expose the calendar state.

A template’s whole schedule (subscriptionTradingDays + strikeLagTradingDays + (observationCount - 1) * observationSpacingTradingDays) is bounded to 90 trading days. setTemplate rejects the quote token and any underlying whose decimals() is not 18 (InvalidTemplate("underlying is quote"), InvalidTemplate("underlying decimals")), the same rule createSeries enforces.

Governance funds the bounty pot with fund(amount) and can sweep it back. registerSeries carries an existing Subscription series into the list (close within 90 days, later than the last entry, fewer than 10 open); unregisterLast removes the tail entry once its series is Cancelled or Unwound, which is how a wrongly registered far close is cleared.

fact Cost of a roll grows with the schedule length: each trading day of the schedule is one or more calendar reads. Measured on a regular calendar, a template with the longest allowed schedule (90 trading days) costs about 1.37M gas to preview and about 1.73M gas to roll; the deployment’s default template (5 + 1 + 15 trading days) is well under a tenth of that. rollAll runs the walk once per template, so a deployment with many long-schedule templates uses rollRange(start, count) windows rather than one rollAll; checkUpkeep performs the same walk per registered underlying and is sized with rollRange-style sharding in mind.

Rolled series have no SeriesGauge by default; governance adds one when desired.

DeskPolicy

fact DeskPolicy is the only address allowed to call Desk.fillCoupon (Desk.filler, a binding that changes only through the protocol governance with the published delay). It holds one rule per underlying and a USDG bounty balance, and turns unmatched SHIELD into a Desk fill on demand:

fact DeskPolicy Reference
Rule fieldMeaning
minBarrierBps, maxBarrierBpsThe series barrier must lie inside this range (both strictly between 0 and 100%)
minCouponFloorBpsThe series coupon floor must be at least this
maxObservations, maxTenorAt most this many observations and this many seconds from subscriptionEnd to the last one
minCouponBpsThe lowest coupon the Desk accepts on the underlying; the fill is sized so the discovered coupon stays at or above it
enabledThe guardian can disable an underlying instantly

Parameters: fillLead (window before subscriptionEnd in which fills are allowed, default 24 hours, 1 hour to 7 days), minFill (smallest fill, default 100 USDG, 100 to 10,000 USDG and never below the bounty), minFillInterval (per series cooldown, default 1 hour, 10 minutes to 24 hours) and bounty (per successful fill, at most 1,000 USDG and never above minFill, paid min(bounty, balance)). Anyone may fund the bounty balance.

Bounty grinding is bounded: over any W seconds with k series in Subscription at most k × (W / minFillInterval + 1) bounties can be earned, each backed by a fill of at least minFill, and every bounty comes from the policy balance, never from the Desk.

fill(seriesId) (nonReentrant, whenNotPaused) checks NotSubscription, RuleInactive, OutsideFillWindow, SeriesOutsideRule (barrier, coupon floor, observations or tenor), FillCooldown and DeskUnavailable (Desk or core paused, or the series feed unusable), then reads Desk.previewFill for the SHIELD gap S - D and the Desk’s room (the three cap headrooms and free idle USDG net of the withdrawal queue). The window also closes at the effective strike close when an early close brings it before subscriptionEnd (reason WindowClosed).

The target is the largest COUPON deposit that keeps refBps * S / D at or above minCouponBps (the whole gap when the series floor already meets the hurdle, nothing when the hurdle is above the series cap), further capped by notionalCap and by the notional the SHIELD prefund can pay for (NoteCore.leastFundedShield); the fill is min(target, room) and must reach max(minFill, minTicket) (NothingToFill otherwise, preview reason BelowMinFill).

The policy then calls Desk.fillCoupon(seriesId, amount, minCouponBps): the Desk posts a reserved deposit on NoteCore (depositCouponReserved), and at strike the core matches it only as far as the discovered coupon stays at or above the hurdle, refunding the excluded part to the Desk (ReserveExcluded); so however the SHIELD side moves between the fill and the strike, the matched part of a Desk fill never earns less than the hurdle.

The Desk independently enforces its caps, its idle and amount <= S - D (FillExceedsGap), so the vault can never be pushed past the SHIELD side whatever a caller computes. previewFill(seriesId) returns (amount, canFill, reason) without reverting, and canFill is true exactly when fill would succeed in the same block.

The Desk is therefore a price-sensitive counterparty rather than a backstop: SHIELD demand that would push the coupon below the hurdle stays unmatched, and a series with no COUPON deposits at all still strikes when the rule and the caps allow the fill.

fact NoteAutomation implements the Chainlink AutomationCompatibleInterface (checkUpkeep(bytes) -> (bool, bytes) / performUpkeep(bytes)). checkUpkeep scans up to maxSeriesPerCheck (default 256) series via dueWork (the newest maxSeriesPerCheck ids, [seriesCount - maxSeriesPerCheck, seriesCount), when checkData is empty, or the range abi.encode(from, to) for sharded upkeeps, clamped to maxSeriesPerCheck ids from from; a range beyond the last series is clamped to an empty window rather than reverting), the Minter week and every registered underlying’s previewRoll, the registered Desk’s active series (a Harvest is due once a series with deployed principal has left Subscription and Live) and encodes a list of actions {Strike | Observe | MintWeekly | Roll | Harvest}. Strike and Observe actions are queued only while the core is not paused; MintWeekly only while the Minter is not paused, the week is not yet minted and the NOTE token is not paused; Roll only while both the core and the RollPolicy are not paused. Every module read in the scan is wrapped in try/catch, so one module whose view reverts drops out of the list without blanking the rest. performUpkeep executes at most maxActionsPerPerform (default 24) actions inside try/catch (one failing action never blocks the others, UpkeepAction(kind, id, success, reason) is emitted per action). An action counts as successful only when it changed state: a Strike or Observe must move the series status or nextObservationTs (a replayed or deferred call is reported with reason NoProgress), a Harvest must clear the Desk’s deployed principal for that series, MintWeekly and Roll must not revert. The caller is paid min(USDG balance, successes × keeperRewardCap) (RewardsForwarded), so a batch of no-ops pays nothing and splitting a batch into single actions pays the same as one batch; keeperRewardCap defaults to 25 USDG and is set by the owner with setKeeperRewardCap(cap) (KeeperRewardCapSet). USDG above the cap stays in the contract and the owner can move it with sweep(to) (Swept).

status Chainlink Automation and Gelato are not yet live on Robinhood Chain. Until they are, anyone can run checkUpkeep/performUpkeep from a wallet, a Safe module, a Defender/OpenZeppelin relayer or a simple bot; the interface is unchanged when a network becomes available. Nothing in the protocol depends on it: the lazy path guarantees liveness on its own.

Registration guidance. A single registration with empty checkData stays useful at any series count because it always covers the newest ids, and strikes and observations are only ever due on recent series (settled series produce no work). With daily issuance, the number of series that can need a strike or an observation at the same time is about templates x schedule trading days (Subscription and Live series both count).

Add sharded registrations with explicit abi.encode(from, to) windows once that product exceeds maxSeriesPerCheck (for example more than two templates on a 90 trading-day schedule), or to keep a dedicated upkeep on a long-lived older series.

fact 57 feeds are published for Robinhood Chain mainnet (chain id 4663). All are 8-decimal proxies with a 24 h heartbeat and 0.5 % deviation threshold; the equity feeds follow us_equities_24/5 market hours. Source: contracts/deploy/chainlink/feeds-robinhood-mainnet.json (mirrors the Chainlink feed directory).

fact Mainnet Chainlink Data Feeds (Robinhood Chain) Reference
FeedProxyHeartbeatThresholdDecimals
AAPL / USD0x6B22A786bAa607d76728168703a39Ea9C99f2cD086400 s0.5%8
AMD / USD0x943A29E7ae51A4798823ca9eEd2ed533B2A22C7286400 s0.5%8
AMZN / USD0xD5a1508ceD74c084eBf3cBe853e2C968fB2a651C86400 s0.5%8
ASML / USD0xB4106147E8cce40b7d46124090d373A71b70f87D86400 s0.5%8
BABA / USD0x62Cc8F9b5f56a33c9C8A60c8B92779f523c4E98486400 s0.5%8
BTC / USD0xa2c5184bF03d373Dc9dE4876eb4Bce595B46025186400 s0.5%8
BTC.B / USD0x5BB5e6a17a477d5B6Fec77b4322daD4A66bFb73286400 s0.5%8
CBBTC / USD0x0009cD492adf8167f9eEBf1293556A673530a21a86400 s0.5%8
CLSK / USD0x810c12D3a554Bc47fd39597Fe3b3AAC4941F50eF86400 s0.5%8
COIN / USD0xA3a468A452940B7D6b69991207B508c609a98Ef286400 s0.5%8
CRCL / USD0x6652eDf64bA3731C4F2D3ce821A0Fb1f1f6b482a86400 s0.5%8
CRWV / USD0xe1b3aABCAFAd1c94708dc1367dcfF8Aa4407487C86400 s0.5%8
DELL-USD0x1C6c8cADBe02E19129c39dDB92281cE4c0bf206b86400 s0.5%8
ENA / USD0x2A291496b3aa19d8948e442Ef28Ee952f3Ee97E886400 s0.5%8
ETH / USD0x78F3556b67E17Df817D51Ef5a990cDaF09E8d3A986400 s0.5%8
EURC / USD0xfF2B10c1973eD10c841434f98e456d8f3a0D7DD886400 s0.5%8
EWY / USD0xEFdf54610B62A7753Ec30bDc380847c12D32e1D186400 s0.5%8
GME / USD0x27C71df6A64fB476468EdF256CF72c038baB5B6786400 s0.5%8
GOOGL / USD0xF6f373a037c30F0e5010d854385cA89185AE638b86400 s0.5%8
INTC / USD0x3f390C5C24628Ac7C489515402235FeAD71D191386400 s0.5%8
IONQ / USD0x22EfeC4919baf55F360E0EDee4AbEB26DE4971eb86400 s0.5%8
LBTC / USD0xa621344AdAEE699491597Fd8890E0C59a5BFBE5986400 s0.5%8
LINK / USD0xe86e3422Aa9B5e8ee9f3E41a63975bC387A8bce986400 s0.5%8
META / USD0x7C38C00C30BEe9378381E7B6135d7283356D71b186400 s0.5%8
MSFT / USD0x45C3C877C15E6BA2EBB19eA114Ea508d14C1Af2E86400 s0.5%8
MSTR / USD0x396118bdFB181e6240E74D243F266B061c0edc3D86400 s0.5%8
MU / USD0x425EEFdCf05ed6526C3cE61Af99429A228a6d59686400 s0.5%8
NBIS / USD0xE1D87B116Ba0fe898998f1D140339D1fA1E0970586400 s0.5%8
NVDA / USD0x379EC4f7C378F34a1B47E4F3cbeBCbAC3E8E9F1586400 s0.5%8
ORCL / USD0x0e6a64a2B58A6693a531E6c555f3A5d042eEA84486400 s0.5%8
PLTR / USD0x820ABedFF239034956B7A9d2F0a331f9F075eB4c86400 s0.5%8
QQQ / USD0x80901d846d5D7B030F26B480776EE3b29374C2ae86400 s0.5%8
RGTI / USD0x2A045cF1C49c61c166C036d2f06FA2D2d984f76586400 s0.5%8
RKLB / USD0x045477BF65Aef6f4F2386ad0164579e48381CC7486400 s0.5%8
SGOV-USD0xa0DF4ee0fFf975306345875E3548Fcc519577A1186400 s0.5%8
SLV / USD0x209b73908e92Ae021826eD79609845451Ecba2ce86400 s0.5%8
SNDK / USD0xfb133Fa4B7b385802B693a293606682Df47109A386400 s0.5%8
SPCX / USD0xB265810950ba6c5C0Ff821c9963014a56fD8Bffb86400 s0.5%8
SPY / USD0x319724394D3A0e3669269846abE664Cd621f9f6A86400 s0.5%8
SYRUPUSDC / USD0x8765c3B9Cda41d1029E780D0c1C37C8200DC467586400 s0.5%8
SYRUPUSDC / USDC Exchange Rate0x6317f016FA3e312C4625dee51d32b43a223011f886400 s0.05%18
SYRUPUSDT / USDT Exchange Rate0xBB688c0184Ce03fEdac89D71ccE752Ab21bC299986400 s0.05%18
TSLA / USD0x4A1166a659A55625345e9515b32adECea5547C3886400 s0.5%8
TSM / USD0x874cF94aa8eC88Fd9560094dD065f2fB3E41Fc2F86400 s0.5%8
USAR-USD0xA994d3684e8400A6c8078226925779FdeE682DD986400 s0.5%8
USDC / USD0x9e6f4605992a899eE2999999F3Ec80C41F45254686400 s0.5%8
USDE / USD0xb9fB4e65744E4178894f7C61CF80E8a48A5f224a86400 s0.5%8
USDG / USD0x61B7e5650328764B076A108EFF5fa7282a1B9aD286400 s0.5%8
USDS / USD0x2D88D75b625633dCcd65d9d53BfDD3Aea2d8e84f86400 s0.5%8
USDT / USD0xbf3550B6fAe1671da7C238Af12e03Ac586BEf3B186400 s0.5%8
USO / USD0x75a9c76Ef439e2C7c2E5a34Ab105EcFe3766431c86400 s0.5%8
WBTC / USD0x62107b0d3adA75fc1697fD342d99eed947a3aA5E86400 s0.5%8
WEETH / EETH Exchange Rate0xb63f44E40aA811Cc69Fc55da786a5F3834100B4A86400 s0.05%18
WEETH / USD0xf882e1D50352aecB0Ac85378378918BCf40511e786400 s0.5%8
WSTETH / STETH Exchange Rate0x8E3Eb706B170c8FD1DdcD402932D952887736f9A86400 s0.05%18
WSTETH / USD0x3F5040B50FB37934573B210fE54B53a6F1A792E886400 s0.5%8
syrupUSDG / USDG Exchange Rate0xDd194C66aDcb422F188a04434e4824D70c151cF086400 s0.05%18

Note: there is currently no HOOD / USD feed in the directory. Pyth Pro does publish HOOD (feed id 1182), so a HOOD series on mainnet would need governance to accept a PythProFeed as the primary; until then it is not templated. Every one of the 35 stock feeds above has a matching Pyth Pro feed id and a live stock token contract; the joined and on-chain-verified registry is contracts/deploy/oracle/feeds.mainnet.json, and the Pyth side is described on the Oracle page.

Testnet Caveat

testnet only Robinhood Chain testnet (chain id 46630) has no Chainlink stock feeds. The testnet deployment uses eight MockChainlinkFeed contracts whose prices are pushed by the feed admin (deployer, KEEPER_ROLE on the mock). This is a test fixture, not part of the protocol: on mainnet the series feed is the Chainlink proxy from the table above and nobody can write to it. Testnet series also run in SHORT mode (subscription 3 trading days, 4 observations spaced 5 trading days) so a full cycle completes in about four weeks.

Failure Handling

  • Competition. Two callers submitting for the same observation: the first succeeds, the second reverts (the index has advanced). Submit with a modest gas price and accept the occasional revert; NoteAutomation swallows it inside try/catch.
  • Paused core. observe, finalizeStrike and lazy cranks are inactive while paused (dueWork returns no due work). Observations are not lost; they run when unpaused, and the adapter still selects the same closing print as long as fewer than maxRoundWalk rounds were published since.
  • Deferred observations. Neither the lazy path nor NoteAutomation retries a Deferred observation blindly; dueWork reports it due again only when the oracle would accept it, or once governance has cancelled it (in which case the next observe records ObservationSkipped and pays the reward).
  • Paused RollPolicy / disabled template. No new series is created for that underlying; existing series are unaffected.
  • Paused DeskPolicy / disabled rule / paused Desk. No Desk fill for that underlying; the series strikes on organic deposits or cancels at subscriptionEnd exactly as it would without a Desk. NoteAutomation reports the skipped Fill with the policy’s reason and continues with the rest of the batch.
  • Roll gated by the core. When governance lowers maxCouponCapBps or maxObservations below a stored template, or moves seriesCreator away from the policy, previewRoll reports CoreLimits or NotSeriesCreator and roll reverts with the matching error instead of a core revert; the template is updated with setTemplate or the role restored, and rolls resume. A CalendarGap reason means the calendar needs the coming trading days registered before a roll can build a schedule.
  • Collapsed observation. When two observations roll onto the same close, the observe call for the second index (explicit or lazy) records it as collapsed (ObservationCollapsed) and advances nextObs, with no oracle read and no keeper reward. The call is cheap and never deferred; NoteAutomation reports it as NoProgress only if another caller consumed the index first.
A little more clarity?

Keep exploring, or revisit this topic from the navigation.