Your plugin can remember things between refreshes. Read what you saved last time, save something new, and show the history on your screen.
Why?
Private plugins are forgetful by design. Every refresh fetches data, renders it, and overrides (deletes) the previously cached merge variables. This is fine if your API returns everything you need, but it's a problem if the API only tells you what is true right now.
If you need to draw a trend from a single number, or ask an API for what changed since the last data fetch, State is for you.
Prerequisites
A private plugin using the Polling, Static or Webhook strategy. Async Polling never runs a transform, so it cannot use saved state.
A
run()function on the Serverless tab (docs) of the markup editor. This is the only place state can be written. Any of the four languages will do.
Clarification on terminology: trmnl.state and trmnl_state are the same resource, but are different in their usage.
- Read trmnl.state in Markup and in Serverless.
- Write trmnl_state within Serverless.
Step 1 - Read what you saved last time
Your transform receives saved state at input.trmnl.state. On the very first run, and on a fresh install, it is an empty object. Always guard for that.
var history = (input.trmnl.state && input.trmnl.state.history) || [];
Step 2 - Save something for next time
Return a key named trmnl_state from your transform. TRMNL takes that key out of your merge variables and stores it. Everything else you return reaches your template as usual.
return { subscribers: input.subscribers, trmnl_state: { history: updated } };
Whatever you do not write back is gone. If you only save the latest value, next run you have two points and no more. If you want a trend, save the whole series and trim it yourself.
Step 3 - Display it on a screen
Saved state is available in your markup as trmnl.state, so you do not need to return the same data twice.
{% for point in trmnl.state.history %} {{ point.value }} {% endfor %}
It also works in mashup slots, so a plugin that draws a trend full screen draws the same trend in a half slot.
Step 4 - Fetch only what changed
Your Polling URL, headers and body are all templated, so a saved cursor can go straight into the request. This turns a poll that downloads everything into one that downloads only the new rows.
https://api.example.com/events?since={{ trmnl.state.cursor | default: "" }}
Note the default filter. On the first run there is no cursor, and a URL with an empty parameter is usually what you want.
Step 5 - Compare with what you showed last time
Sometimes you do not need a history, only the previous value. A price that went up or down, a timestamp that changed, a count that moved. Copying every such value into state on every run gets old fast.
Your transform also receives input.trmnl.previous_merge_variables. That is the set of merge variables your last run stored, the same values your markup rendered last time. It costs nothing and you did not have to plan for it.
var lastPrice = (input.trmnl.previous_merge_variables || {}).price;
Reading it is all it does. Returning it changes nothing. If you want a value to survive, it goes in trmnl_state.
For a Webhook plugin this is the stored set after your merge strategy ran, so with deep merge or stream it includes everything accumulated so far.
Step 6 - You're done!
The timing is the one part worth reading twice. State is read at the start of a run and written at the end. Your Polling URL and your transform both see the state from the previous run. Your markup sees what the transform wrote this run. That is what you want in both places, but it surprises people who expect the URL to see the value they set a moment ago.
Troubleshooting
State never appears. Check your plugin's strategy. Polling, Static and Webhook plugins run a transform, and a transform is the only thing that can write state. Async Polling plugins have no way to save anything.
State stopped updating. There is a limit of 8192 bytes. Go over it and TRMNL ignores the write, keeps the last good state, and writes an error to your plugin's logs with the exact size. Trim your data in the transform, most often by keeping the last N items rather than everything.
You returned trmnl_state and nothing was saved. It has to be an object. A string, a number or an array is ignored the same way an oversized one is, and your previous state is left alone.
Your data source went down and your state is unchanged. That is deliberate. When a fetch fails, your transform still runs, but against an empty payload. Writing state from nothing would overwrite good data with garbage, so TRMNL skips the write entirely and keeps what you had.
You saved a bad value and every request now fails. A wrong cursor makes every later request wrong, and Force Refresh will not help you, because it deliberately leaves saved state alone. Open the markup editor and use Clear Saved State. It appears only when there is state to clear.
Your previous merge variables are empty. Force Refresh clears the stored merge variables of a Polling or Static plugin before it fetches again, so the run right after one sees an empty object. Saved state is untouched, which is why anything you must keep belongs there.
You installed someone else's recipe and expected their data. State belongs to your install, not to the recipe. Your cursor is not their cursor, and every new install starts empty.
