Development Choices

Automating Asset Expiry and Retention Without Code

Author
Joseph TrasattiMember of technical staff
Published
Section
No-Code
Length
7 min read3 sources cited

Automating asset expiry means writing an expiry date into a structured metadata field at upload, running a scheduled flow that restricts access when that date passes, and deleting only after a gap long enough to catch mistakes. Storage is billed as a current total, so the saving is immediate, but derived copies and backups may keep some of it.

Before you start

Retention is a legal question before it is a technical one. A flow that deletes customer uploads after 90 days encodes an answer — to a contract term, a privacy regulation, a marketing consent window — that somebody with authority has to have given. Get that answer in writing, with the number of days and the categories it applies to, before building anything. The automation is cheap to change; the consequence of deleting evidence a client was obliged to keep is not. If you have already automated deletion of user-generated media, you have this answer for one asset class; the same document should now cover the rest.

You also need somewhere to write an expiry date the automation can read. That means a structured metadata field on the product environment. Cloudinary’s structured metadata fields are typed and defined once per product environment, so a date field is queryable in a way a free-form tag or the upload timestamp is not. Create it before the first asset that needs it is uploaded; a field added later leaves the existing library without values, and backfilling from the upload timestamp is a guess, not a retention decision.

Finally, be able to answer the question “who references this asset?” for at least the main consumers — CMS, marketing site, email templates. You do not need a complete graph; you need to know where a broken image would be seen first.

Steps

  1. Define the expiry field and make it mandatory where retention applies. Create a date-typed structured metadata field — call it expires_at or something the legal document uses. The reason it has to be written at upload rather than inferred is that the upload timestamp is when the file arrived, not when its permission to exist runs out. A press photo licensed for six months and a UGC clip covered by a 30-day consent window can arrive on the same day. Only a value set by the process that knows the licence can be right, so the upload path — your CMS plugin, an upload preset, or a flow step that computes the date from an asset category — has to set it. If a class of asset has no expiry, leave the field empty and treat empty as “retain”; never default it to a date.

  2. Decide the two-stage schedule: restrict first, delete later. Deleting an asset breaks every URL that references it, immediately and everywhere. Because the same URL is embedded in pages, emails already sent, and third-party embeds you cannot recall, expiry in practice means restricting access on the expiry date and deleting on a second date, with a gap long enough for someone to notice a broken image and object. Choose the gap from how quickly your consumers surface problems: a site with monitoring on image 404s can use days; one that relies on a customer complaint should use weeks. Write the gap into the same retention document, because a 14-day grace period is itself a retention decision.

  3. Build the flow that runs on the expiry date. In MediaFlows, start from the prebuilt automations in the sample PowerFlows — the UGC deletion PowerFlow is the closest shape, since it already selects assets by a condition and acts on them. Adapt it so the selection reads expires_at and matches assets whose date is in the past and which are not yet restricted, and so the action is a restriction of access rather than a delete. What “restrict” means depends on how you deliver: at minimum, move the asset out of the state your consumers fetch, and record that you did so — a second metadata field such as restricted_at — so the delete stage has a date to reason from. Have the flow write a log line or notification per asset; the point of the gap is that a human can act during it, which requires knowing it started.

  4. Build the flow that runs after the gap. A second flow selects assets where restricted_at is older than the grace period and deletes them. Keep it separate from the first rather than one flow with a wait, so that pausing deletion during an incident does not also pause restriction. If your team already routes assets through approval inside a flow, consider putting the delete stage behind the same approval step for the first month; once nobody has vetoed a deletion in a full cycle, remove the gate.

  5. Watch the storage figure and understand what it will and will not do. Storage is billed as a current total, one credit per gigabyte, per Cloudinary’s credits FAQ — unlike transformations and bandwidth, which are metered over a rolling 30-day window and age out on their own. That means expiry shows up in the bill straight away: the day the delete flow runs, the storage line drops. It also means the reverse; a bulk upload counts in full from the moment it lands, with no 30-day smoothing.

    The figure will not always drop by the amount the dashboard suggested, because backups and derived copies count toward storage too. Deleting the original removes its bytes; the derived transformations already generated from it, and any backup copy, are separate stored objects, and whether the delete flow removes them depends on how it is configured. If your first run recovers noticeably less than the sum of the original file sizes, that is the reason, and the fix is in the delete step’s options, not in the selection. Check the storage total the day after the first deletion run and reconcile it against what the flow reported deleting.

  6. Handle the assets that predate the field. Anything uploaded before expires_at existed has no value and is retained by default under step 1. Do not backfill it by adding a fixed number of days to the upload timestamp; that reintroduces the guess the field was created to avoid. Instead, treat the legacy library as one retention decision: either the legal answer says it can all be kept, or a one-off flow — the same selection logic as step 3, on a tag marking the legacy set — is run once with a human reviewing the list. If you also run duplicate detection, run it on the legacy set first; you may find much of what you were about to review is copies.

  7. Test with one asset before enabling the schedule. Upload one asset with expires_at set to yesterday. Run the restrict flow manually; confirm the asset is no longer served where your consumers fetch it, and that restricted_at was written. Set restricted_at back past the grace period, run the delete flow, and confirm the asset — and, if you configured it, its derived copies — is gone and the storage total moved. Only then turn on the schedules. This is also where a webhook handler you wrote yourself would need the same test; the hosted flow does not exempt you from it, it just puts the test in a UI.

What done looks like

Every asset subject to a retention rule carries an expires_at value set at upload by the process that knows the licence, and assets with no rule carry none. Two scheduled flows run on the product environment: one restricts access to assets past their expiry date and records when it did so; the other deletes assets that have been restricted for longer than an agreed grace period. Both log what they acted on. The retention periods, the grace period, and the treatment of pre-existing assets are written down and signed off outside engineering. The storage line in the bill falls on the day the delete flow runs, and the amount it falls by has been reconciled once against what was deleted, including derived copies and backups, so the number is understood rather than assumed.

Sources

  1. structured metadatacloudinary.com
  2. sample PowerFlowscloudinary.com
  3. credits FAQcloudinary.com

See also