Overview
Ladyada is so busy making exciting new products for us all, that she needs an extra pairs of eyes at her disposal to catch all the things going on that day. Occasionally she doesn't have time for the latest potty training session with her little one... Now that Adafruit IO Actions have new superpowers, she's getting new reports setup to help keep her informed!
Do you want to track as important events are occurring, like your child's potty training progress? Learn the basics of Adafruit IO Actions with this "Potty Training Report" project.
Use Adafruit IO Actions to send a scheduled report, control LEDs, count button events / progress and reset the counters each day. Use your existing data feeds or follow this arcade button journey for inspiration.
With a wireless microcontroller running WipperSnapper, Adafruit's no-code platform (an open source Arduino firmware) which can easily send data to the cloud, and a couple of buttons to generate events. You'll be firing data to the cloud and getting those daily training reports as quick as a flash.
This project is a great demo for how to use Blockly-based Actions to create full-featured apps that don't require you to write a single line of code or host any services.
Why WipperSnapper and Adafruit IO Actions?
Adafruit IO can accept data in many ways, but there is a special Devices page for easy installation and configuration of WipperSnapper based devices.
It allows any supported Wireless microcontroller to easily install the needed software, using the web browser, and then to configure any of over 100 components that may be attached, like sensors and buttons. There's even an automatic configuration option for any onboard components!
As well as accepting data, Adafruit IO can generate data too. Using Actions. IO can automatically respond and calculate, or with the IO+ paid upgrade you also gain access to Apple WeatherKit data (and other benefits).
Required Parts
In this project a Wi-Fi enabled Raspberry Pi Pico has been used (with headers for no soldering), mounted on a Terminal PiCowbell to give 40 screw terminals along with a reset button and Stemma QT connector!
For this project you'll want 4x Illuminated Push buttons, also known as Arcade Buttons (51mm Square version), and then either solder wires directly to the metal contacts on the arcade button switch units, or use four wires with spade connectors per switch unit.
Or purchase individual Arcade buttons:
If you prefer to not solder wires directly to the metal contacts of the switch unit, use 0.25inch (~6.3mm) sized Spade connectors for the LED connection tabs, and 0.187" (~4.8mm) spade connectors for the button switch tabs, although I found the 6.3mm spades also worked fine for both as the switch tab increases width further back.
Adafruit sells some cables in packs of ten pairs, and you'll need a pair of both sizes for each arcade button.
The JST connectors on the other end are not used and will be cut off, but do offer a quick no-soldering solution. They are designed for the Arcade button STEMMA QT breakout (support coming to WipperSnapper Q4 2025).
Page last edited June 24, 2025
Text editor powered by tinymce.
Wiring
If you're happy creating your own circuit for the LEDs and button switches, as it's often one of the first things you learn in electronics, then skip over this page. Otherwise read on!
This project uses a lot of wires, so color coding or labelling them can help (tape / pen / colored heat-shrink). It uses all of the ground connections available on the Raspberry Pi Pico, but other configurations are possible.
In fact, only one wire on each switch needs careful management: the positive line of the LED, as the LED has a fixed polarity (direction).
First find the LED direction / polarity.
Starting with just one arcade button for now, connect the larger sized (0.25" / 6.3mm) spade connectors onto the metal tabs at the two larger sides of the switch unit (marked LED in the image).
The other ends of the wires need to be cut and stripped (>2mm), then insert one of the two wires into a ground connection on the board. To make life easier, the back of the Terminal PiCowbell has big white bands labelling the ground connections. Make sure you have undone the screw gate first! Tighten the wire screw gate.
Take the second wire and insert it into the 3.3 volt connection on the Raspberry Pi Pico, and then power the Pico using a USB cable. If the LED is illuminated then you got lucky, otherwise the wires need to be swapped around.
Unplug the board from USB power, and also the LED wire connected to 3.3 volts, then label the two wires you've just identified as positive (3v3) and negative (ground). The button switch connections are not polarised, so the direction doesn't matter, just connect wires to them.
Your arcade buttons switch unit should now have four wires connected, with one of those routed to ground.
Repeat this procedure for four all of the arcade buttons.
Wire up the circuit as described (shown above). You can vary the pins as they don't need special functionality.
Connect the positive wires for the LEDs to pins GPIO 2 onwards (3, 4, 5) ideally keeping them in button order.
Later, during testing of the lights, it'll be easier to identify which is which again and swap wires if needed.
Turn the Pico Terminal board 180 degrees around so you're now looking at pins GPIO 18-21. It's not necessary, but I found it easier to hold and wire up this way.
Connect the first wire from each of the button switches to GPIO 18 onwards (19, 20, 21).
Finally, wire the remaining four switch wires to ground pins.
Page last edited June 24, 2025
Text editor powered by tinymce.
Having light up buttons without custom labels feels like a wasted opportunity, and tying back in with the Potty Training theme, I've used some appropriate Emoji labels (recommended to me 😁) for the buttons.
This project will count number 1's (💦) and number 2's (💩), when nothing came (❌), and "told an adult" (🔔).
Grab your emojis, or images, then paste them in a document or your favourite graphics program. You can copy them from a webpage (they're in the paragraph above) or another page / app.
You can also hand type emojis! I used the Windows Key and Period (.) to bring up the Emoji keyboard (Command + Control + Space on mac), and then searched for the emojis. Try searching for 'sweat', 'poo', 'cross', and 'bell'.
Issues? Try emailing yourself emojis using your mobile phone, and then copy from the email once back on the computer.
Use a large font size (I used 80pt) and print them out.
If you find the characters/emojis are not displaying correctly in your software then try changing the font, or switching software.
You're aiming for images/labels that are a maximum of 37.4mm (1.472" or 1 15/32") width and height, but you can make them with a larger edge and use the inside diffuser piece of the button as a template for cutting.
Simple printed paper works fine, as do sticky labels, but transparent versions might block less light.
I've used whole A4 or Letter sheet sized labels suitable for inkjet printers and added some extra emojis for later projects.
Read the guide (linked below) for details on how to safely remove the button from it's case.
I used a plastic dental spatula (like a spudger) to help the clips over the inner lip, and to then carefully split the inner button to remove the transparent lid and reveal the label location.
Place the label into the transparent button lid, or apply the sticker to the inner button light diffuser (with the domed back side, resting on the tweezers in this image), then reassemble the arcade button.
It can a be a tight fit if the label is poorly trimmed, so use a moderate amount of force if needed. It will then be extra hard to remove again, but not impossible if you carefully lift from the corners.
If you need a bit of help with assembly and disassembly of the buttons, then check this guide dedicated to Arcade Buttons:
Make a case - 3D Print / Laser cut / Drill
If you're making your own case then you'll need to mark out a template for the holes to cut, or use CAD.
The arcade buttons have two nubs / pegs approximately 4 mm in diameter and 42 mm from furthest edge to furthest edge (38 mm center to center). They stop the button being rotated, but can be cut off.
Then you need a 26mm hole for the big screw thread, or 23.5-24 mm with a chamfered edge at the front.
The spade terminals add a few extra millimeters to an already deep bolt and switch unit, approximately 60mm from the back of the button to the end of the spade plus a sharply bent (almost 90 degree) wire.
While reassembling later (after dealing with mounting to the wall), it may be easier to remove one or two spade connectors that are close to the struts.
Prototyping is important, as it's a risky idea to assume everything will work out perfectly first attempt.
A friend and I had great fun testing a single button version of the potty trainer. It's top is too flexible, although that is still strangely enjoyable!
Initially I used the laser cutter, due to its rapid speed, to see how the fit and spacing felt. A quick project box followed, with top plate for hygiene reasons.
Switching back to PETG for 3D printing, and using a Brim for support, the case prints on it's side for maximum strength, with the Terminal PiCowbell clips closest to the bed. I printed with a 0.6mm nozzle.
Here is the 3D model in STL format, oriented correctly:
My first clips for the Terminal PiCowbell worked well, but self-destructed in the CAD model when I moved them.
It's important to be thoughtful about relative references in parametric CAD designs. Use offset planes and variables based on other dimensions if possible.
The second version now works when the PiCowbell is moved in the design, but I initially forgot the offset for easy insertion. Now it has that satisfying ease and a good snap into place.
You can modify the model configuration, but you'll need to setup an OnShape account (free for public designs).
Attaching to the wall
Listen when Ladyada says something! She told me under no circumstances use double sided tape for the project, as people have so many problems with poor quality tape or humidity affecting stickiness.
It's quite a heavy project to be sticking on the bathroom wall, so Ladyada suggested using strong magnetic strips, or magnetic tape, instead of risking it with double-sided tape.
Using the magnet strips is a great idea as they are really easy to apply and super strong, while also being removable!
They usually arrive in pairs, magnetically attracted to each other, with sticky on the back sides.
Simply leave the two magnetic strips attracted together with the sticky side on the outside / backs, then remove one piece of backing paper and stick that side to the project enclosure.
Locate the first pair of strips at the top edge of the back side.
Use a second pair too, along the bottom edge this time.
Leaving the backing paper in place is a good idea until final installation.
Now bring your project into the final installation location. Remember that this project is only splash resistant, so should be positioned accordingly. Remove the remaining backing paper, and stick the project to the wall.
Apply a good amount of force and squidging where the sticky magnetic strips are located. I like to imagine squeezing bubbles out of the newly applied sticky magnetic tape. I also removed the wiring first!
Leave it for a minute or so before testing the removal.
After you're done potty training, the same enclosure can be turned on its back, some grippy feet attached, and you have a wonderful table-top button console! Why not try CircuitPython and make a custom controller!
Page last edited June 24, 2025
Text editor powered by tinymce.
WipperSnapper Setup
Installing WipperSnapper should be a quick and easy process, but it's still under development so tell us if you run into issues. If you know how to use a Serial Monitor, the device spits out useful info over the serial port!
Head to the Devices page on Adafruit IO, and click the New Device button.
Select your board from the list of compatible boards, then follow the instructions to get the software and secrets (WiFi and Adafruit IO credentials) copied to your board.
Using a Chromium based browser works best with the web installer (Chrome / Edge), but local installation using UF2 files (or esptool) is supported too!
If your board isn't listed in the board selector then you can request support by creating an issue on GitHub (or just try the most compatible looking boards by chipset and pin count).
After installation and copying the secrets (not required for boards without USB Mass storage device support), the board should reboot itself.
If not then reset it manually (using a reset button or by reconnecting power).
Make sure you have an Adafruit IO webpage open, as it's required to receive the Registration message, and then name the device once it connects.
If you missed this registration / board naming step, don't worry, just reset the board and try again.
After registration you're taken to the Device page for the newly registered board. From here you can view the Online Status, WipperSnapper version installed, previous connection events, and any components you've added to the device. There is also a Settings button to change the device name, or delete the device (and data).
Adding components is a simple process, click + or the New Component button, then search for the component you're looking for.
The search isn't perfect, so in addition to searching for the name or family, try other similar words, the type of data, the I2C address, or the categories like PWM, analog, i2c, uart, pin, digitalio.
You'll then be asked to configure any options for your component, such as pin/address, brightness, any pull-up resistors, or at least the name.
Each new component has it's own row, and an automatically associated data Feed, one per data type (e.g. temperature + humidity).
It's worth noting that some additional display related options are present in the component settings after creation, like the on/off state labels and icons for buttons.
The components feed page is linked via the Graph icon at the end of the row, next to the component's Settings cog icon.
Once you've configured a device, you can Export the configuration so it can be shared or easily recreated / duplicated.
There's a complimentary Import config option too, and if the board doesn't match, or component addresses are in use, then you may be asked to unselect those incompatible components before import.
Almost all boards have some built in components, like an on-board LED, NeoPixel, or boot button.
Most of these are useable by WipperSnapper, and can be automatically setup using the Auto-Config button, then Magic Configuration.
Configure Project Components
You need to add eight components to your new device. Each Arcade Button needs setting up as a button component with a pull-up resistor, with an additional component for each button LED (pick dimmable LEDs although setting them up as normal LEDs is also fine). You can do this really easily by importing a config file!
{
"exportVersion": "1.0.0",
"exportedBy": "tyeth",
"exportedAt": "2025-06-10T18:13:03.071Z",
"exportedFromDevice": {
"board": "rpi-pico-w",
"firmwareVersion": "1.0.0-beta.100"
},
"components": [
{
"name": "💦 Wee Button",
"pinName": "D18",
"type": "push_button",
"mode": "DIGITAL",
"direction": "INPUT",
"period": 0,
"pull": "UP",
"isPin": true
},
{
"name": "💦 Wee LED",
"pinName": "D2",
"type": "led",
"mode": "DIGITAL",
"direction": "OUTPUT",
"isPin": true
},
{
"name": "💩 Poo Button",
"pinName": "D19",
"type": "push_button",
"mode": "DIGITAL",
"direction": "INPUT",
"period": 0,
"pull": "UP",
"isPin": true
},
{
"name": "💩 Poo LED",
"pinName": "D3",
"type": "led",
"mode": "DIGITAL",
"direction": "OUTPUT",
"isPin": true
},
{
"name": "❌ Didn't Go Button",
"pinName": "D20",
"type": "push_button",
"mode": "DIGITAL",
"direction": "INPUT",
"period": 0,
"pull": "UP",
"isPin": true
},
{
"name": "❌ Didn't Go LED",
"pinName": "D4",
"type": "led",
"mode": "DIGITAL",
"direction": "OUTPUT",
"isPin": true
},
{
"name": "🔔 Tell Adult Button",
"pinName": "D21",
"type": "push_button",
"mode": "DIGITAL",
"direction": "INPUT",
"period": 0,
"pull": "UP",
"isPin": true
},
{
"name": "🔔 Tell Adult LED",
"pinName": "D5",
"type": "led",
"mode": "DIGITAL",
"direction": "OUTPUT",
"isPin": true
}
]
}
Use this configuration file to easily import all the needed components for this project, the four buttons plus their dimmable LEDs. Find the Import button by using the Auto-Config button on the device page.
If you'd prefer to set the components up manually, then have a look at the Usage page of the Quick Start Guide for Using The Raspberry Pi Pico With WipperSnapper which covers most types of components:
After setting up the components, they can be configured further using the Settings cog at the end of the row.
You can view the feed page and data associated with each component by clicking the Graph icon there too.
Before continuing on to create your Actions, ensure you're device page looks similar to the image below, with all four buttons and dimmable LEDs configured.
Page last edited June 24, 2025
Text editor powered by tinymce.
Create Adafruit IO Actions
In this project, you'll create a whole bunch of actions, but don't worry, it's only three plus a bit of repetition:
- When the first button is pressed, turn on the LED, and increase the first "Counter" feed by 1
- When the first button is pressed, delay a few seconds (3), then turn the LED off.
- Repeat previous two actions for remaining buttons (each with their own counter feed)
- At a scheduled time each day, collect the counter feeds and use in an email template, then reset.
Hopefully that doesn't sound too complex, and it's worth noting you can do similar with any data on Adafruit IO.
Create the Counter feeds
When counting things on Adafruit IO you'll need somewhere to store that count. It's possible to use a variable as a temporary count holder, but variables only live inside the action briefly as that action executes, and are then destroyed again. Alternatively using another Feed is a more permanent storage option.
As you'll want to interact with the counter from more than one action it needs to be stored in a Feed.
Go to the Feeds page
Use the New Feed button, and then enter a memorable Name for the feed (the only restriction is length). I've used "💩 Poo Counter" as the name, which gives me a feed key of poo-counter.
The new feed will show up in the Default group at the top of the feeds list (which I've renamed to "My Feeds").
Repeat the process to create the remaining counter feeds (one per button). For simplicity name them wee-counter, nothing-counter, and informed-counter.
Initially the feeds have no data, which will cause errors to display during the first run of the email template. If you'd prefer you can go and manually add data to the counter feeds (like zero, or some other test number).
Add a new Action
Start by visiting the Actions page using the link in the navigation menu, where you'll see a list of your actions.
It shows their names, descriptions, enabled status, and an information icon button with more details.
Click the New Action button, enter a suitable name and optional description, e.g. "Button #1 pressed - increase counter", then submit.
You'll be brought to the new Action page and see a workspace in the centre with a single "root block" and toolbox.
It's a diagram based "programming" tool, similar to other low-code/no-code platforms.
There is a toolbox and a main workspace area where you compose your Action, attaching blocks to the root block to form your idea.
Triggers and Cooldowns
The root block has a Triggers: section that defines the different events which can trigger the action. You can use multiple if you like, and the action will fire when any of the triggers match their condition (schedule or data).
The root block also has a Settings button (an icon with a grey cog in blue background), used for configuring delayed execution (which can also optionally reset existing delays to create a dead man's switch / fail-safe).
The other crucial bit of information is about cooldown periods. After a trigger matches, and then the action is executed, a cooldown period is activated (currently five seconds) during which all triggers are ignored.
Start by opening the Triggers category in the toolbox, and pick the trigger that says When FEED receives data that STARTS matching = 0.
Drag and drop it onto the Triggers: section of the root block, making sure to line up the puzzle piece edges (the notches and tabs) until the root block shows a yellow highlight.
Then to configure the block, use the Feed selector drop-down menu to select the first button Feed.
Next leave the comparison Operator selected on equals, and set the Value to 1.
Great, that's the trigger done. Now onto the Actions: section of the root block, where you'll select the "doing" blocks for your action.
Start with a Set Feed block from the Feeds category in the toolbox, drag + drop it into the Actions: section.
Select the first buttons LED feed from the Feed selection drop-down.
Enter the value 1 into the Set Feed blocks value textbox.
If using a Dimmable LED component, instead of plain LED, then use the value 255 for max brightness.
Next you'll be setting the first button's counter feed to increase, using the Set / Get Feed blocks and a block from the Maths category of the toolbox.
Start by grabbing a Set Feed block and dropping it beneath the previous one.
Select the appropriate counter feed created earlier in the Set Feed blocks feed selector drop-down menu.
Now go to the Maths category in the toolbox, and grab the block that shows the plus/addition operator, then place that maths block into the Value section of the Set Feed block you've just placed.
Enter the value 1 into one of the two value text boxes inside the Maths block, and leave the operator as Plus (+).
Now grab a Get Feed Value block from the Feeds toolbox category, and finally drop it into the remaining value box of the maths block just placed.
Your action should look like this before continuing. Save the action, and choose enable if asked.
For the next action, to turn off the LED again, it's nearly identical to the first steps of the last one. The only difference is related to delayed execution, which you can configure using the root blocks Settings dialog.
Create a new action, called something like "#1 Button - LED off after 3seconds".
Setup the same trigger as before, When Button 1 receives a value that Starts matching = 1.
Use the same Set Feed block to change the #1 Button LED feed value to 0.
Now comes the interesting part, setting the delay for the Action.
Start by clicking the grey settings cog on the root block, and a new dialog opens, it's like a mini Blockly workspace just for configuring the delay options.
Drag the block for 1 Seconds over to the right-hand side (the mini workspace area), to replace No Delay.
Pick an appropriate amount of time in the drop-down menu for the seconds, like 3.
Finally close the delay settings dialog by clicking on the same settings cog again.
You action should look like this, and can now be saved.
Repeat the previous two actions for each of the remaining buttons, using their dedicated LED + Counter feeds.
Before continuing, double check that the Actions page now shows the 8 Actions just created, enabled in the list.
On the next page you'll create the Action to send the scheduled email report, and then on the subsequent page learn about templates and the specifics for this project.
Page last edited June 24, 2025
Text editor powered by tinymce.
Email Action with Variables
An action needs a trigger. Let's use the Schedule trigger block to activate daily at 20:15.
Drag the Schedule block from the Triggers category of the toolbox, snapping it into place inside the Triggers: section of the root block.
Use the Settings cog (grey cog in blue background) for each of the Schedule block's time and date segments to build the schedule.
Change the month and day segments to Every Day and Every Month, and the hour and minute segments to suitable time values like 8:15 PM.
That's the trigger section done! Now you'll setup the rest of the action, but with no explanation of the template. This is to save overcomplicating things in one mega page, instead you'll learn about templates afterwards.
Before adding the email block to the Actions: section of the workspace you'll create some variables, doing some seemingly pointless maths to make it easier to be referred to later in the email body. This is for two reasons, firstly because all data in Adafruit IO is effectively stored as text strings, explained later on the next page.
Secondly, it is possible to use feed values directly in templates as you'll see later, but crucially the feed has to be used in a block, somewhere in the action, to then be available for templates.
There are a few things in the template that could also be calculated as variables first, like total events.
The example template calculates everything in the template, unwisely, just to show how it can be done.
Start by creating a new variable using the button at the top of the Variables category in the toolbox.
Call it poo_progress, then create the remaining 3 variables, wee_progress, nothing_progress and informed_progress.
Drag a Set Variable block into the main Actions: section of the workspace diagram, and select the variable.
Grab one of the Addition blocks from the Maths category in the toolbox, and drop it into the value section of the Set Variable block. Leave the operator as plus / addition (+), and the values untouched at zero.
Use the Get Feed block from the Feeds category, and drop into one of the the value sections of the Maths block.
Select the feed for the first counter (e.g. poo_counter) from the feeds dropdown in the Get Feed block.
Now grab a second Set Variable block and drop it beneath the first, selecting the wee_progress variable, and add the maths and feed blocks for the associated counter feed wee-counter.
Repeat this process for the remaining counter feeds, at which point your action should look like this:
Next it's time to reset the counter feeds. You'll use the Set Feed block to set the counter values to zero.
Start by adding a Set Feed block beneath the previous blocks, then select the poo-counter feed from the dropdown, setting the value to 0.
Repeat for the remaining counter feeds. Your action should look like this before continuing:
Add an Email block from the Notifications category in the toolbox. Enter a subject for the email.
From the Text toolbox category select the Template block (with the {{ label) and drop it into the value section of the second Set Variable block.
Update the Template's child Paragraph text block, by clicking inside it and updating the value to use the following template body text. Then click outside the text area to finish setting the block content.
<b>🌟 Daily Potty Training Report 🌟</b>
Hi there! Here's how our little superstar did:
---
<b>🚽 Successful Wees: {{ vars.wee_progress }}</b>
{% for i in (1..vars.wee_progress) %}💧{% endfor %}
{% if vars.wee_progress <= 5 -%}
Great start! Every success counts, and they're building good habits one wee at a time! 🌱
{% elsif vars.wee_progress <= 15 -%}
Fantastic progress! They're really getting the hang of this - keep up the amazing work! 🎯
{% else -%}
SUPERSTAR ALERT! 🌟 Absolutely crushing it with those wee successes! They're a potty champion! 🏆
{% endif %}---
<b>💩 Successful Poos: {{ vars.poo_progress }}</b>
{% for i in (1..vars.poo_progress) %}🟤{% endfor %}
{% if vars.poo_progress == 0 -%}
Poos can be tricky, but they're being so brave! Every try is a step forward! 💪
{% elsif vars.poo_progress < 2 -%}
Look at them go! They're becoming a real poo pro - that's awesome progress! 🎉
{% else -%}
POO CHAMPION! 🏅 They've mastered one of the trickiest parts - so proud! 🎊
{% endif %}---
<b>🤝 Told an Adult: {{ vars.informed_progress }}</b>
{% for i in (1..vars.informed_progress) %}🗣️{% endfor %}
{% if vars.informed_progress <= 5 -%}
Communication is key! Keep practicing saying when they need to go - They're doing great! 📢
{% elsif vars.informed_progress <= 15 -%}
Wonderful communication skills! They're really good at letting us know - that's so helpful! 👏
{% else -%}
COMMUNICATION SUPERSTAR! 🌟 They're amazing at telling adults - that's such a big kid skill! 🎯
{% endif %}---
<b>👻 Nothing Happened: {{ vars.nothing_progress }}</b>
{% for i in (1..vars.nothing_progress) %}⭕{% endfor %}
{% if vars.nothing_progress <= 3 -%}
That's okay! Trying is what matters, and their body will let them know when it's ready! 🌈
{% else -%}
So patient and persistent! Even when nothing happens, they keep trying - that's real determination! 💫
{% endif %}---
<b>📊 Daily Summary:</b>
{% capture total_tries -%}{{ vars.wee_progress | plus: vars.poo_progress | plus: vars.nothing_progress }}{% endcapture -%}
{% capture successes -%}{{ vars.wee_progress | plus: vars.poo_progress }}{% endcapture -%}
{% capture success_rate -%}{% if total_tries != "0" -%}{{ successes | times: 100 | divided_by: total_tries }}{% else -%}100{% endif -%}{% endcapture -%}
{% capture bar_filled -%}{{ success_rate | divided_by: 10 }}{% endcapture -%}
{% capture bar_empty -%}{{ 10 | minus: bar_filled }}{% endcapture -%}
Total potty visits: {{ total_tries }}
Success rate: {{ success_rate }}% [{%- for i in (1..bar_filled) -%}█{%- endfor -%}{%- for i in (1..bar_empty) -%}░{%- endfor -%}]
{%- assign total_events = vars.wee_progress | plus: vars.poo_progress | plus: vars.nothing_progress | plus: vars.informed_progress -%}
({{ total_events }} events today )
{% if total_events <= 3 %}
💝 <b>Today:</b> They're doing such a great job learning! Every day gets a little easier...
{% elsif total_events <= 5 %}
🌟 <b>Today:</b> Ayee! They're really getting the hang of this potty training thing! Keep it up!
{% elsif total_events <= 8 %}
🌟 <b>Today:</b> WOW! Look at all that practice! They're becoming such a potty expert.
{% else %}
🏆 <b>Today:</b> INCREDIBLE DAY! They're absolutely rocking this potty training journey! 🎊🎉
{%- endif %}
Keep being awesome!
With love and high-fives! 🙌
---
{%- assign event_mod = total_events | modulo: 3 %}
{% if event_mod == 0 -%}
<i>P.S. Remember: Every expert was once a beginner - they're doing brilliantly! 🌟</i>
{% elsif event_mod == 1 -%}
<i>P.S. Fun fact: Even superheroes had to learn to use the potty! 🦸</i>
{% else -%}
<i>P.S. Remember: accidents are just practice in disguise! They're doing amazingly! 💕</i>
{% endif %}
Before continuing, your action should now look nearly identical to this:
Save and Run the action, then check your email for the sample report, it should look something like this:
Now head on over to the final page to learn more about the template, and how you can craft your own!
Page last edited June 24, 2025
Text editor powered by tinymce.
"Liquid" Templates
Templates in Adafruit IO Actions are done using the Liquid templating language. It has built in support for some helper functions, or "filters" as they are known in liquid parlance.
All template blocks require a child block to be attached, with either a text string block, or paragraph block, or something else text compatible (like a Get Feed value block).
Every line in the template gets whitespace (line-breaks) at the end of it, but that can be prevented by adding the dash (-) syntax to any placeholders ({{- placeholder -}}) or commands ({%- assign t = vars.now -%}).
It can be used to trim / collapse the whitespace from just the beginning, or end, or both. For example {{ vars.now }} prints the time with a newline afterwards, but using {{- vars.now -}} removes whitespace before and afterwards. Alternatively {{ vars.now -}} would just remove from the end.
Using feed values as part of a template allows you to easily add units and symbols, or display messages, to dramatically improve the user experience. Templates can be used in place of text values for any of the blocks.
The classic example is to add a prefix and suffix for a dashboard label, or sending an email report, to yourself, based on some triggering condition (reacting to incoming data) or schedule.
There are a few built-in Adafruit IO custom template "placeholders". These allow easy insertion of the user related properties (like name) using {{ user }}, variables created inside the action using {{ vars }}, and any feeds used in the action under {{ feeds }}. Of course the variable must be set before use in a template.
Accessing the sub-properties of these placeholders is done with the dot-notation, e.g. {{ user.name }}, or square bracket indexing / accessors like python uses, e.g. {{ user["name"] }}.
Bracket notation is preferred, as it supports any properties with unusual characters (like dots . and spaces).
For the feeds placeholder, you access each feed as a sub-property using the feed key (dash separated lowercase), e.g. a feed called Feed-Key in a group named Group-Key would be feeds["group-key.feed-key"]
Then you want to access the sub-property .value, or another like name, last_value, updated_at, etc.
e.g. {{ feeds["group-key.feed-key"].value }} would be replaced with the feed value inside a template.
The feed key can be found using the URL when visiting the feed page, or using the feed info button, or lastly by logging/emailing the value of {{ feeds }}. See this guide on Editing Feeds for details on feed keys:
https://learn.adafruit.com/adafruit-io-basics-feeds/editing-a-feed
With templates referring to feed values the syntax can become very long and almost unreadable, but you can help yourself by creating variables first from the needed feeds, and then refer to the variables instead.
Using Filters and basic logic in Liquid templates
Spend a few minutes reading about basic usage and the list of filters on the official Liquid help documentation:
The template uses a couple of filters to do some basic arithmetic (plus:), repeating characters using for-loops (based on the counter value), and printing phrases of encouragement based on certain if conditions.
Filters are quite simple at their most basic usage level, and chained together using Pipes (vertical bar |), for example to make your name a lowercase version: {{ user.name | downcase }} [Docs for downcase]
The idea for this template was to repeat an emoji the same number of times as the counter, as a sort of bar graph / progress indicator. With a small nearly-identical section of the email template for each of the counters.
Finally the template totals all the events recorded by the counters, along with a hypothetical success rate. Then based on some predefined ranges, gives an appropriate summary and phrase of encouragement. Of course after the daily report is sent the counters should be reset to zero.
Some of these might be clearer if calculated as variables first, like total events, and then used in the template.
The example template calculates everything in the template, unwisely, just to show how it can be done.
The first piece of the template matching the counter for button 1 (did a wee) has two simple bits, print the value, then repeat emoji X times.
🚽 Successful Wees: {{ vars["wee_progress"] }}
{% for i in (1..(vars["wee_progress"])) %}💧{% endfor %}
Here you can probably tell that the first line outputs the current value of the variable, and the second line repeats the character 💧 up to the number of times in the variable's value with a "for i = 1 to X, repeat💧".
If you want to have a summary phrase in each section of the report, then you can add lines like these too:
{% if vars.wee_progress == 0 %}
Slow progress is still progress, keep that encouraging reinforcement going!
{% elsif vars.wee_progress <= 2 %}
Getting the hang of this, keep up the amazing work! 🎯
{% else %}
SUPERSTAR ALERT! 🌟 Absolutely crushing it! You've got a potty champion! 🏆
{% endif %}
This section is then repeated for the remaining progress variables, with slightly different phrases & thresholds.
You'll no doubt notice that the template is doing numeric comparison of the variables (that you added zero to).
All feed data on Adafruit IO is stored as strings (text encased in speech marks). We previously did a bit of automatic conversion in some places, but now with the new Actions we leave the power (and complexity) in your hands...
Having all data as text strings is normally fine to use in templates, however if doing comparisons it gets tricky, but there are a couple of methods for converting (casting) text data to numerical data instead.
Firstly, Maths blocks in the toolbox allow you to manipulate numbers, automatically casting strings, so use + 0.
Secondly, liquid templates support their own form of variables, both capturing and assigning, using the syntax {% capture name %}value{% endcapture %}(captured as strings), and {% assign name = value %}. Some maths filters convert text strings automatically, like plus:, as does the range syntax (1..number) used in the template.
This template uses capture for a few extra template variables, calculating the total tries versus successes, creating a percentage bar of success from repeating ASCII greyscale characters.
Then finally, the total_events is divided by 3 and the remainder then assigned to event_mod using | modulo: 3. The last phrase is conditional based on the remainder in event_mod, a bit like "Quote of the Day".
Now imagine if you will, saving the value of total_events and successes, to new feeds, using Variables for the templated values, then the Set Feed block. You could even have total_events_last_week, 2weeks ago, etc.
Your report could include your sensor data or weather data too, maybe that dramatically impacts productivity...
Then with weekly, monthly, etc, hopefully your mind also begins to play with other non potty ideas...
Remember that you get all sorts of benefits with IO Plus, like unlimited feeds and devices! And don't forget to checkout the Weather block, exposing the full Apple® WeatherKit data for your added locations.
A Real Liquid Template Debugger
If you wish to experiment with liquid templates in a more constructive environment, then try this online liquid template editor and debugger (I've included the variables with the template for easier testing):
Page last edited June 24, 2025
Text editor powered by tinymce.
Downloads
Page last edited June 24, 2025
Text editor powered by tinymce.