A private plugin can hand its data to an AI model instead of a script. You write a prompt in plain English, the model answers on every refresh, and the answer becomes the merge variables your markup renders. You can also let the model write the whole screen, or ask one plugin to summarize what several other plugins fetched.
The model runs on your account, not ours. It uses the API key you add under Account, or TRMNL's own key against your AI credit, which every device owner gets. Each refresh costs a fraction of a cent on the default model.
Prerequisites
A TRMNL device, or an API key for OpenRouter or Anthropic under Account, then Agent.
A private plugin. Any strategy works: polling, async polling, webhook, static, plugin merge, or none.
Step 1 - Pick Prompt (AI) as the language
Open your private plugin, click Edit markup, and open the Serverless tab. Set the language to Prompt (AI). The box below it takes the prompt.
The prompt is Liquid, like your markup. Whatever the plugin fetched is available by name, so a polling plugin that returns a list of events can say "Summarize {{ events | json }} in one line each". The model gets the rendered prompt and replies with a JSON object, and each key in that object becomes a merge variable.
Step 2 - Shape the reply
Two lines of front matter between two --- lines keep the reply predictable. Leave them out when you are experimenting.
schema: a JSON schema the reply must match. The model fills exactly those keys, so your markup can count on them.
max_tokens: a cap on the reply, up to 4096. A reasoning model spends part of this thinking before it writes, so 300 is enough for a sentence and too little for a list.
format: text, when you want prose instead of JSON. The reply arrives as one merge variable named text.
A complete example. The model must answer with a headline and a list of items, so the markup can count on both keys:
---
schema:
type: object
properties:
headline: { type: string }
items:
type: array
items:
type: object
properties:
label: { type: string }
value: { type: string }
required: [headline, items]
max_tokens: 800
---
Here is today's data as JSON: {{ events | json }}Write a headline of at most six words and up to four items, each a short label with its value.
The quickstart buttons on the Serverless tab insert this example when the language is Prompt (AI).
Save, and the preview on the right renders once the model has answered. The first render after a change shows the last reply while the new one is on its way.
Step 3 - Write the markup
Your markup reads the merge variables the way it always has. For the schema above, that is {{ headline }} and a loop over items:
<div class="layout layout--col gap--small">
<span class="title">{{ headline }}</span>
{% for item in items %}
<div class="flex gap--small">
<span class="label">{{ item.label }}</span>
<span class="value value--small">{{ item.value }}</span>
</div>
{% endfor %}
</div>
<div class="title_bar"><span class="title">{{ trmnl.plugin_settings.instance_name }}</span></div>
With format: text the model may answer in markdown. Ask for a heading and bullets, then render it with the markdown_to_html filter: {{ text | markdown_to_html }}.
The model can also write the screen itself. Set the strategy to none, ask for an HTML fragment with its CSS in a style tag, and wrap it in your markup so its styles stay inside: a div holding a template with shadowrootmode set to open, with {{ text }} inside. This is what the AI Dashboard plugin does, and with a prompt of your own you can do the same for any subject.
Step 4 - Chain plugins
A prompt works on one plugin's data. To summarize several, create a private plugin with the Plugin Merge strategy and name the plugins it should read in the prompt, with the same private_plugin_123 names the merge markup uses. The weather plugin keeps polling its API, and the merge plugin turns the latest result into a briefing.
The plugin page shows the chain as a Data flow strip: the plugins this one reads, this one, and the merges that read it. Each card links to that plugin.
Step 5 - You're done!
Add the plugin to a playlist. The model is asked again only when the rendered prompt changes, so a polling plugin whose data did not move costs nothing on that refresh. A plugin with no data source asks on every refresh, since a fresh reply is the point.
Your AI credit shows on the plugin page while a prompt plugin uses TRMNL's key, with a button to reload it.
Troubleshooting
The device shows "Out of AI credit".
Reload credit from the plugin page, or add your own API key under Account, then Agent. The plugin refreshes on its own once credit is back.
"Add an API key in Account, or connect a TRMNL device, to run this plugin's prompt."
TRMNL's key is only for accounts with a device. Add an OpenRouter or Anthropic key to run prompts without one.
"The reply ran past N tokens and was cut off."
Raise max_tokens, or remove it to allow the full 4096. A cut-off reply is never stored, so the screen keeps its last data.
The model is asked on every refresh even though the data looks the same.
Something in the rendered prompt changes each time. Many APIs include a timestamp or a request time in their reply. Pass only the fields the prompt needs, such as {{ data.daily | json }} rather than {{ data | json }}.
"The prompt's front matter has an option this plugin does not know."
Only schema, max_tokens and format are read. A model or temperature setting is not, since the model is the one your account uses.
The reply is prose but my markup expects keys.
Add a schema. Without one the model may answer in text, and a text reply becomes a single variable named text.
My merge plugin's prompt gets no data from the plugin it names.
Check the name: it is private_plugin_ followed by the plugin's id, and the Data flow strip on the plugin page lists what it reads. If the strip is missing, the name is not matching anything.






