FAQ content vs FAQ schema — what actually helps more for SEO today?

While working on AllInOneTools, I’ve been thinking about FAQ sections from an SEO + AI perspective.

Earlier, the common advice was:

👉 Add FAQ schema to get rich results.

But now with AI Overviews and answer-based search, things feel different.

Because AI seems to care more about:

• clear questions
• simple answers
• structured content

Not just schema markup.

So now I’m wondering:

👉 Is the real value in the content itself, not the schema?

From what I’ve seen:

• FAQ content helps with understanding + context
• Schema helps with formatting + visibility

But AI might prioritize:

👉 clarity over markup

Now I’m curious 👇

What do you think matters more today?

• Well-written FAQ content
• FAQ schema markup
• Or both equally?

Have you seen any real difference in results?

Would love to hear real experiences.

I Scanned 300 Vibe-Coded Repos. The #1 Finding Will Annoy You.

TL;DR

  • Hardcoded secrets (CWE-798) show up in roughly 2 out of 3 AI-generated repos
  • It happens because AI models were trained on years of tutorial code, not production code
  • A pre-commit hook with gitleaks catches this in under 5 seconds

I’ve been scanning repos for a few months now. Mostly side projects, a handful of production apps that founders shared with me directly. The pattern I keep seeing is secrets hardcoded directly into source files.

Not occasionally. Not in old projects. In code that was written last week, sometimes yesterday, by developers who absolutely know better.

Here’s the thing: they didn’t write it. Their AI did.

The Vulnerable Pattern

This is the exact snippet I’ve found in some variation across maybe 200 of the ~300 repos I’ve scanned:

// Generated by Cursor, March 2026
const jwt = require('jsonwebtoken');

const SECRET = 'my-super-secret-key-123';

function generateToken(userId) {
  return jwt.sign({ id: userId }, SECRET, { expiresIn: '7d' });
}

CWE-798, use of hardcoded credentials. The JWT secret is sitting in plaintext in a file that will get committed to version control. If this is a public repo, it’s already exposed. If it’s private, it’s one accidental visibility change away from being exposed.

I see variations with database connection strings containing passwords, Stripe secret keys, OpenAI API keys, and AWS access credentials. All in the source. All committed. Gitleaks flags them in about 2 seconds.

Why AI Keeps Doing This

AI code generators were trained on an enormous corpus of public code. A huge portion of that corpus is tutorial content, blog posts, README examples, Stack Overflow answers. That code almost universally hardcodes values. It’s easier to read. It makes sense for a demonstration.

The model learned from that. When it generates a JWT auth example, it reaches for the pattern it has seen thousands of times: const SECRET = 'some-string'. It’s not being lazy. It genuinely learned that this is how people write this code.

The problem compounds because AI tools are optimized to produce working code fast. A hardcoded secret works. The token generates. The tests pass. Nothing breaks until it breaks badly.

Cursor, Claude Code, Copilot, they all do this. Some do it more than others. None of them reliably stop themselves.

The Fix

Straightforward:

// Pull from environment variables
const SECRET = process.env.JWT_SECRET;

if (!SECRET) {
  throw new Error('JWT_SECRET environment variable is not set');
}

function generateToken(userId) {
  return jwt.sign({ id: userId }, SECRET, { expiresIn: '7d' });
}

Set JWT_SECRET in your .env file. Add .env to .gitignore. Done.

But catching it before commit is better than fixing it after. Here’s a gitleaks pre-commit hook:

# Install gitleaks
brew install gitleaks  # macOS
# or: go install github.com/gitleaks/gitleaks/v8@latest

# Add to .git/hooks/pre-commit
#!/bin/sh
gitleaks protect --staged --no-banner

That hook runs in about 2 seconds and catches hardcoded secrets before they ever hit your repo. For the whole project, not just staged files:

gitleaks detect --no-banner

Scan Your Own Codebase

I’ve been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags these patterns before I move on. That said, even a basic pre-commit hook with gitleaks will catch most of what’s in this post. The important thing is catching it early, whatever tool you use.

AI Agents Are Getting Credit Cards. The Fraud Stack Is Missing.

AI Agents Are Getting Credit Cards. The Fraud Stack Is Missing.

Visa announced AgentCard last month. Stripe built a machine payments protocol. World launched AgentKit for AI identity.

Everyone is racing to give AI agents spending power. Nobody is talking about what happens when agents commit fraud.

The Infrastructure Gap

We spent two years building AI agents that can reason, plan, and execute. Now we are giving them money to spend autonomously. But here is the problem: every payment system built in the last 50 years assumes a human on the other end.

Fraud detection systems look for:

  • Device fingerprints (agents run headless)
  • Behavioral patterns (agents are deterministic)
  • Location anomalies (agents can be anywhere)
  • Spending velocity (agents operate at machine speed)

All the signals that catch human fraud fail spectacularly against AI agents because agents are not trying to hide. They are just executing instructions at scale.

What Agent Fraud Looks Like

Picture this scenario:

An e-commerce company deploys a shopping agent to compare prices across suppliers. The agent finds a “deal” on a supplier website that is actually a clone site. The agent places a $50,000 order. The fraud detection system sees:

  • Valid API credentials
  • Normal purchase patterns
  • Legitimate shipping address
  • No device red flags (because there is no device)

The transaction goes through. No human reviewed it because that defeats the purpose of autonomous agents.

The Missing Layer

We need three things that do not exist yet:

1. Agent Identity Verification
World is trying with AgentKit, but it is opt-in. A fraudster can spin up an agent that does not participate in any identity framework.

2. Agent Reputation Scoring
Payment processors need a way to score agents, not just merchants. An agent with a history of disputed transactions should have limits.

3. Agent Liability Frameworks
When an agent commits fraud, who pays? The agent operator? The agent creator? The company that deployed it? Current payment terms of service do not cover this.

What Needs to Happen

The industry is moving fast. Visa, Stripe, and others are building agent payment infrastructure. But fraud prevention is an afterthought.

Before we give agents credit cards, we need:

  1. Cryptographic agent identity – not just API keys, but verifiable agent certificates
  2. Agent spending limits – agents should not have unlimited purchasing power
  3. Human-in-the-loop for high-value transactions – autonomy does not mean zero oversight
  4. Agent audit trails – every transaction should be traceable back to the instruction that triggered it

The Real Question

The question is not whether AI agents will commit fraud. They will. The question is whether we will have the infrastructure to detect it, attribute it, and recover from it.

Right now, we do not.

As we race to give agents spending power, let us make sure we are not building the same payment rails with none of the safety mechanisms.

The payments infrastructure for AI agents is being built right now. The fraud stack is what keeps me up at night.

GDPR and IoT Devices: Privacy Obligations for Connected Product Manufacturers

Smart home sensors that log when you leave for work. Fitness trackers that infer your menstrual cycle from heart rate data. Connected cars that build a map of everywhere you drive. Smart plugs that reveal home occupancy patterns precise enough to determine when a property is empty. Industrial sensors that monitor individual workers’ movements and productivity.

The Internet of Things is, in many ways, the largest personal data collection apparatus ever built — operating continuously, invisibly, and in the most intimate spaces of people’s lives. For manufacturers and product companies building these devices, GDPR creates a set of obligations that differ meaningfully from those facing web businesses. The stakes are higher, the technical constraints are different, and the regulatory expectations have evolved significantly.

This guide covers what GDPR actually requires from connected product manufacturers — from device design through data sharing, firmware updates, and the intersecting requirements of the EU Cyber Resilience Act.

What IoT Devices Collect — and Why It’s Personal Data

The first compliance question for any IoT manufacturer is deceptively simple: what does our device collect, and does any of it constitute personal data under GDPR?

The answer is almost always yes — and often more extensively than initially apparent.

Location data is an obvious example. GPS-enabled devices — cars, fitness trackers, asset trackers — generate continuous location histories that are, by definition, personal data. But location can be inferred from devices that don’t have GPS. Smart home devices that connect to a home Wi-Fi network reveal the home address. Smart meters reveal not just energy consumption but, when analysed at sufficient granularity, occupancy patterns, appliance usage, and daily routines.

Behavioural data emerges from almost any IoT device. A smart plug records when specific appliances are switched on and off. A smart lock records entry and exit times. A connected coffee machine records when someone wakes up. Individually, each data point might seem trivial. Aggregated over weeks and months, these streams build a detailed picture of an individual’s daily life.

Health inferences are particularly sensitive. Smart scales don’t just measure weight — they calculate BMI, track trends, and increasingly infer metabolic health indicators. Fitness trackers measure heart rate variability, sleep stages, stress levels, and activity patterns. These devices generate health data that qualifies as special category data under GDPR Article 9, triggering significantly stricter processing requirements.

Voice data and ambient audio collected by voice assistants represents some of the most intimate data an IoT device can capture. Even devices that are theoretically “always listening only for the wake word” create significant privacy exposure — both because of the volume of audio inevitably captured and because recordings are often processed remotely.

Industrial IoT raises workplace privacy considerations. Sensors tracking individual worker movements, productivity metrics, or biometric data on a manufacturing floor are processing personal data, and the power imbalance between employer and employee means consent is rarely a valid legal basis.

Who Is the Data Controller?

For web services, the data controller question is usually straightforward. For IoT products, it can be genuinely complex — and getting it wrong has direct compliance consequences.

Consider a typical smart home device: the device itself is manufactured by one company, the companion app is operated by another (or by the same company’s software division), the cloud backend where data is processed and stored may be a third entity, and the device may integrate with third-party services like Google Home, Amazon Alexa, or Apple HomeKit.

Under GDPR, the data controller is the entity that determines the purposes and means of processing personal data. Where two or more parties jointly determine purposes and means, they are joint controllers under Article 26, with specific obligations around transparency and inter-controller arrangements.

In practice:

  • If the manufacturer controls both the device firmware and the cloud backend, they are likely the sole controller for core processing activities.
  • If the manufacturer’s app sends data to a third-party analytics platform, that platform is a data processor — and the manufacturer needs a valid data processing agreement.
  • If the device integrates with a smart home ecosystem like Google Home, there may be joint controller arrangements to document and disclose.
  • If a white-label device is sold under another brand’s name, the question of who controls the data requires careful legal analysis of the actual data flows.

Manufacturers often underestimate their controller responsibilities when they sell through retailers or license their technology to other brands. The data subject — the person using the device — will hold someone accountable when things go wrong. GDPR requires that accountability to be clearly allocated before that happens.

Purpose Limitation in AI-Enhanced IoT

One of the most significant compliance challenges for modern IoT products is purpose limitation. GDPR Article 5(1)(b) requires that personal data be collected for specified, explicit, and legitimate purposes and not further processed in a manner incompatible with those purposes.

The problem: IoT devices increasingly use machine learning to find secondary uses for the data they collect. A fitness tracker sold for activity monitoring might, in later firmware versions, add stress detection, sleep coaching, or fertility tracking. A smart home hub sold for energy management might add occupancy prediction. A connected car’s telematics system might be repurposed for insurance scoring.

Each of these secondary uses potentially requires a new legal basis. If the original consent was for fitness tracking, it may not cover health inferences. If the original legitimate interest assessment covered smart home automation, it may not cover marketing analytics derived from occupancy data.

Before launching secondary processing use cases, manufacturers need to ask:

  1. Is the new purpose compatible with the original purpose (applying the Article 6(4) compatibility test)?
  2. If not, what is the legal basis for the new processing?
  3. If the new basis is consent, do users need to be re-asked — and what happens if they decline?
  4. Have we updated our privacy notice to disclose the new processing?

This has real product consequences. You cannot simply ship a firmware update that adds new data processing capabilities and treat existing users as having consented by virtue of not opting out.

Firmware Updates and Consent

This brings us to one of the most practically challenging questions in IoT privacy compliance: can you update processing purposes through a firmware update?

The short answer is no — not without a valid legal basis for the new processing, appropriate disclosure, and, where consent is required, fresh consent.

When a manufacturer ships a firmware update that changes what data is collected or how it is used, they are potentially changing the purpose of processing. Users who consented to the original processing have not consented to the new processing. Legitimate interest claims for the new processing require a fresh balancing test.

Best practice for manufacturers:

  • Version your privacy notices — each major release that changes data processing should be accompanied by an updated privacy notice, communicated to users via the app or email.
  • Distinguish between functional and data-processing changes — a firmware update that fixes a security vulnerability does not require fresh consent. A firmware update that adds a new analytics feature does.
  • Build consent infrastructure into the companion app — make it possible to present users with new consent requests when they open the app after a major update.
  • Respect opt-outs — if a user declines new processing, the device should continue to function for its original purpose without the new data collection.

Voice Assistants and Ambient Data Collection

Voice-activated IoT devices — smart speakers, connected TVs, in-car voice systems — present unique GDPR challenges around ambient data collection.

The technical reality is that distinguishing “wake word detection” from “ambient listening” is difficult to explain to consumers and difficult to prove to regulators. Supervisory authorities across Europe have taken an increasingly sceptical view of claims that voice assistants only process audio after the wake word is detected, given the processing required to detect the wake word in the first place.

Key obligations for voice assistant manufacturers:

Transparency: Privacy notices must clearly explain what audio is captured, how long recordings are retained, whether recordings are reviewed by humans (even on an anonymised basis), and how users can request deletion.

Purpose limitation: Audio captured for wake word detection cannot be retained and used for advertising profiling without a separate legal basis. Multiple regulators have found this practice unlawful.

Access and deletion: Users have a right to access recordings made by their devices and to request deletion. Manufacturers need functional mechanisms for both.

Children: If a voice assistant is in a home with children (and they almost always are), the manufacturer should have considered the likelihood that the device captures children’s voices and addressed this in their DPIA.

Smart Home Data and Article 9: Health Inferences

Article 9 of GDPR provides heightened protection for “special categories” of personal data, including health data, data revealing racial or ethnic origin, and data concerning sex life or sexual orientation.

IoT devices can generate health data — or data from which health information can be inferred — without ever explicitly being health devices. This is one of the most legally unsettled areas of IoT privacy.

A smart scale that measures weight is arguably not processing health data in the Article 9 sense — weight is not inherently a health indicator. But a smart scale that tracks weight trends over time, calculates BMI, and correlates readings with activity data from a paired fitness tracker starts to cross into health data territory. A fitness tracker that detects irregular heart rhythms is plainly processing health data.

Home occupancy data from smart plugs or smart meters may reveal health information indirectly — irregular patterns might indicate illness, disability, or care needs. Supervisory authorities have not definitively ruled on when occupancy data becomes health data, but the risk is real.

For manufacturers of devices that collect or can infer health data:

  • Conduct a DPIA before launch — Article 35 makes this mandatory for large-scale processing of special category data.
  • Identify the correct legal basis under Article 9(2) — explicit consent is the most common, but explicit consent has a higher bar than standard consent.
  • Implement data minimisation at the device level — if you do not need raw sensor data to deliver the feature, do not send it to the cloud.
  • Restrict access to health data within your organisation — Article 9 processing should be limited to those who need it to deliver the service.

Children’s IoT Devices

Smart toys, educational tablets, children’s fitness trackers, and connected learning devices occupy a particularly sensitive regulatory position.

GDPR Article 8 sets the age of digital consent at 16 (member states can lower this to 13), meaning that consent from a child below this threshold requires parental authorisation. But beyond consent mechanics, the broader principle is that children deserve enhanced protection — the ICO’s Age Appropriate Design Code (Children’s Code) in the UK, and equivalent frameworks emerging across the EU, place additional obligations on services directed at children.

For children’s IoT manufacturers:

Verify age and parental consent. Simply asking users to confirm they are over 16 (or whatever the local threshold is) is not sufficient for services clearly directed at children. Robust age verification and parental consent mechanisms are expected.

Data minimisation is not optional. Collect the minimum data necessary to deliver the product’s core features. Do not build analytics or profiling capabilities based on children’s data.

Default to privacy-protective settings. All data processing beyond the core product function should be off by default for devices used by children.

No behavioural advertising. Do not use children’s data for advertising profiling — this is explicitly problematic under the Children’s Code and increasingly under GDPR interpretation.

Location data requires particular care. Smart toys and children’s devices that track location create obvious safeguarding risks if that location data is compromised. Apply strict security controls and avoid retaining precise location histories beyond what is technically necessary.

Data Minimisation in Device Design

Privacy by design, required under GDPR Article 25, means integrating privacy protections into the technical design of a product — not bolting them on afterward.

For IoT manufacturers, this translates into concrete hardware and firmware decisions:

Can processing happen on-device? Voice recognition, activity classification, and many sensor processing tasks can be performed locally, on the device itself, without sending raw data to the cloud. Edge processing is both a performance advantage and a privacy advantage — data that never leaves the device cannot be breached in transit.

Can you collect less? A fitness tracker that needs to detect step counts does not need to retain raw accelerometer data — it can calculate steps locally and discard the underlying data. A smart meter that needs to enable time-of-use billing does not need to retain minute-by-minute consumption data forever.

Can you aggregate before transmitting? Instead of streaming raw sensor data to a cloud backend, aggregate into hourly or daily summaries. This delivers the user value while significantly reducing the personal data footprint.

Can you design out certain data categories? If your device could theoretically collect voice data but your product does not require voice functionality, do not include a microphone. This “privacy by hardware design” approach is increasingly advocated by data protection authorities.

Data Sharing with Third-Party Integrated Services

IoT devices rarely operate in isolation. Most companion apps integrate with cloud services — IFTTT, Amazon Alexa, Google Home, Apple HomeKit, Spotify, weather services, delivery notifications. Each integration is a potential data sharing relationship that requires GDPR compliance.

Processor vs. controller: Where your device sends data to a third-party service that processes it on your behalf (e.g., a cloud analytics provider), that third party is a data processor and requires a Data Processing Agreement under Article 28. Where the third-party determines its own purposes for the data (e.g., an advertising platform), it becomes an independent controller — and you need to disclose this to users.

International transfers: Many IoT cloud services involve transferring data to the US or other third countries. Post-Schrems II and the EU-US Data Privacy Framework, this requires either an adequacy decision, Standard Contractual Clauses, or another valid transfer mechanism. For every third-party integration, manufacturers need to verify the transfer basis.

Disclosure in privacy notices: Every third party that receives data from your device — and the nature of that sharing — must be disclosed to users. Generic “we may share with partners” language does not satisfy GDPR’s transparency requirements.

Security Requirements: Encryption, Secure Updates, and Beyond

GDPR Article 32 requires data controllers to implement appropriate technical and organisational measures to ensure security appropriate to the risk. For IoT devices, this has specific implications.

Encryption at rest: Personal data stored on the device or in the cloud backend should be encrypted. For devices that store data locally (smart cameras, wearables with onboard storage), this means hardware-level encryption.

Encryption in transit: All communication between the device, the companion app, and the cloud backend should use TLS 1.2 or higher. Unencrypted IoT communications are a well-documented attack vector.

Secure boot and firmware integrity: Devices should verify the integrity of firmware before executing it. Compromised firmware that exfiltrates user data is a GDPR breach — and a product liability issue.

Secure update mechanisms: Firmware updates should be cryptographically signed. Update channels should be protected against man-in-the-middle attacks. The update infrastructure itself needs security hardening.

Access controls: Cloud backends processing personal data from IoT devices should implement role-based access controls, multi-factor authentication for privileged access, and audit logging.

Data breach response: Manufacturers need incident response procedures that can detect a device-level or backend compromise, notify users and supervisory authorities within the GDPR’s 72-hour window, and isolate affected devices if possible.

The EU Cyber Resilience Act and Its Intersection with GDPR

The EU Cyber Resilience Act (CRA), which entered into force in late 2024 with compliance obligations phasing in through 2027, adds a layer of security regulation that intersects significantly with GDPR for IoT manufacturers.

The CRA imposes mandatory cybersecurity requirements on products with digital elements — which covers virtually every connected device. Key requirements include:

  • Products must be designed and manufactured with security by default.
  • Manufacturers must provide security updates for the expected product lifetime (minimum five years for most consumer IoT).
  • Known vulnerabilities must be addressed and updates made available without delay.
  • Manufacturers must report actively exploited vulnerabilities to ENISA within 24 hours.
  • A Software Bill of Materials (SBOM) must be maintained for products.

The overlap with GDPR is significant. A device that fails CRA security requirements — say, one that lacks encrypted storage or secure update mechanisms — is also likely failing GDPR Article 32. A vulnerability that is exploited and leads to personal data exfiltration triggers both CRA notification requirements (to ENISA) and GDPR breach notification requirements (to supervisory authorities and potentially to data subjects).

For IoT manufacturers, the practical consequence is that security compliance needs to be planned together — CRA and GDPR have complementary requirements that can be addressed through a unified technical programme rather than two separate compliance exercises.

A Practical Checklist for IoT Manufacturers

Before launch:

  • Complete a DPIA for all personal data processing (mandatory for special category data and large-scale processing)
  • Document every data flow: device to app, app to cloud, cloud to third parties
  • Establish controller/processor roles for all third-party relationships
  • Implement Data Processing Agreements with all processors
  • Design privacy notice to be accessible in the companion app — not just on a website
  • Implement privacy by design: edge processing where possible, data minimisation at source, encryption at rest and in transit
  • Age-gate consent properly for products that may be used by children

Ongoing obligations:

  • Maintain a Record of Processing Activities (Article 30)
  • Version privacy notices with each processing change
  • Obtain fresh consent or establish new legal basis before expanding data use
  • Respond to access, deletion, and portability requests within one month
  • Maintain 72-hour breach notification capability
  • Ship security updates for the product’s supported lifetime

Third-party integrations:

  • Audit all third-party services for their role (processor or independent controller)
  • Verify transfer mechanisms for cross-border data flows
  • Disclose all third-party sharing in the privacy notice

Where to Start

If you are building or operating connected products and want to understand your current privacy exposure, the most useful first step is often a scan of your companion app’s website and backend for visible compliance gaps.

Run a free scan at app.custodia-privacy.com/scan — it takes 60 seconds, requires no signup, and identifies trackers, consent issues, and privacy policy gaps on your public-facing web presence. It will not scan your device firmware or cloud backend, but it will give you a concrete starting point for the compliance work ahead.

The device-level and backend compliance work — DPIAs, data flow mapping, security architecture — requires deeper engagement. But understanding where your public-facing presence stands is a useful and quick first step.

Last updated: March 27, 2026. This post provides general information about GDPR obligations for IoT and connected product manufacturers. It does not constitute legal advice. Regulatory requirements vary by jurisdiction, product category, and specific data processing activities — consult a qualified privacy and data protection professional for advice tailored to your products and circumstances.

From Diapers to Deployment: How I Built a Gamified Pregnancy App in React.

When I was on maternity leave, I noticed a funny dynamic in our household. Like any expectant mother, I downloaded a bunch of pregnancy tracking apps. But my husband—a dedicated gamer—completely boycotted them.

Why? Because the UI was always pastel pink, and the apps constantly compared our unborn child to fruits. He didn’t want to know his baby was the size of a “blueberry” or a “kumquat.” He felt entirely disconnected from the process, like a mere side character in a rom-com.

The Mission: A “Tacticool” Survival Guide

I realized there was a gap in the market: a pregnancy app that actually speaks a gamer’s language. So, during the baby’s nap times, I started learning to code.

My goal was to build a dark-mode, gamified survival guide purely for expectant dads. I called it Partner in Action.

Instead of standard tracking, I designed features that made sense to him:

Hardware over Fruit: Size comparisons use tools.

Recon Operator: Dads upgrade a tactical operator based on the pregnancy’s progress.

AWOL Status: If the app is ignored for 7 days, the dad’s status automatically changes to “Desertion” (this one was my personal revenge!).

Inventory Management: Tracking baby gear exactly like an RPG inventory system.

The Tech Stack: React + Capacitor

Since I was a solo mom-dev learning on the go, I needed a stack that would allow me to move fast and deploy to mobile without learning two different native languages.

I chose React for the frontend because of its massive community and component-based architecture. To get it onto mobile devices, I used Capacitor. It was amazing to see my web code turn into a real, installable Android app.

Of course, it wasn’t without its hurdles. Figuring out the mobile UI quirks, handling state management while sleep-deprived, and making sure the “Cry Detector” audio feature worked smoothly through Capacitor plugins tested my sanity. But seeing the app finally live on Google Play made every late-night coding session worth it.

What’s Next? (The Roadmap)

The app is out in the wild, but the mission isn’t over. I am currently working on a “Postpartum DLC” (a fourth-trimester expansion pack) and a medical jargon glossary to help dads decrypt hospital terminology.

If you are a dev, a gamer, or a dad-to-be, I would love for you to check it out and give me some harsh feedback on the architecture or UI.

Check out Partner in Action on Google Play: https://play.google.com/store/apps/details?id=com.partner_v_akci.app

Have you ever built an app just to prove a point to your partner? Let me know in the comments!

I built a “New Tab” sticky notes app to replace my physical notes

Hey everyone,

My physical desk is covered in real sticky notes. Keyboard shortcuts I keep forgetting, quick hex codes, the occasional inspiring quote, and “call X at xxx-xxxx” reminders.

I tried putting these into my usual note apps, but I never see them. Opening an app just to jot down a temporary thought just didn’t work for me. And it’s just another open program in my cmd + tab bar.

So, I built Sticky Notes FYI.

It’s a lightweight, browser-based app that I use to show on my “New Tab” page. I focused on three things that usually annoy me about modern productivity tools:

Zero friction: No “Sign up to start,” no email verification. It just works immediately.

Markdown by default: Since we’re on dev.to, you’ll appreciate this. It handles checklists and code snippets without fighting the UI.

Privacy: It’s 100% offline-first. Your notes stay in your browser’s local storage. I don’t want your data, and honestly, I don’t want the server costs of hosting it anyway.

The Tech Side:
I used Vue to create it and local storage to keep it snappy. I also added a few themes like “canvas” (my favorite), kanban (if you like to drag stickies around), and “galaxy” (stars move when you move your mouse) because fun. 🙂

If you’re looking for a way to clear the physical post-its off your monitor, give it a whirl. It’s completely free.

I’d love to hear what you think—especially if you have ideas for features that would make your life easier! Or, you know, when you find a bug… shudder <3

NocoBase 2.0 Beginner Tutorial – Chapter 6: Workflows

06-workflows-2026-03-20-13-25-38

Originally published at https://docs.nocobase.com/tutorials/v2/06-workflows

Chapter 6: Workflows — Let the System Do the Work

In the last chapter, we added permissions so different roles see different content. But all operations are still done manually — when a new ticket comes in, someone has to go check; when a status changes, nobody gets notified.

In this chapter, we’ll use NocoBase’s Workflow engine to make the system do things automatically — configure condition checks and update nodes for automatic ticket status transitions and timestamp recording.

6.1 What Is a Workflow?

A workflow is a set of automated “if… then…” rules.

Think of it like an alarm on your phone that goes off every morning at 8 AM. That alarm is the simplest workflow — when a condition is met (it’s 8 AM), an action executes automatically (the alarm rings).

NocoBase workflows follow the same idea:

06-workflows-2026-03-20-13-25-38

  • Trigger: The entry point for the workflow. For example, “someone created a new ticket” or “a record was updated”
  • Condition: An optional filtering step. For example, “only continue if the assignee is not empty”
  • Action: The step that does the actual work. For example, “send a notification” or “update a field“

Actions can be chained with multiple nodes. Common node types include:

  • Flow control: Condition, Parallel branch, Loop, Delay
  • Data operations: Create record, Update record, Query record, Delete record
  • Notifications & external: Notification, HTTP request, Calculation

This tutorial only covers a few of the most common ones — once you learn to combine them, you’ll be able to handle most scenarios.

Trigger Types at a Glance

NocoBase offers several trigger types, which you select when creating a workflow:

Trigger Description Typical Use Case
Collection event Fires when a record is created, updated, or deleted New ticket notification, status change logging
Schedule Fires on a Cron expression or fixed time Daily reports, periodic data cleanup
Post-action event Fires after a user performs a UI action Send notification after form submission
Approval Initiates an approval flow with multi-level support Leave requests, purchase approvals
Custom action Bound to a custom button, fires on click One-click archiving, batch operations
Pre-action event Intercepts a user action synchronously before it completes Pre-submit validation, auto-fill fields
AI Employee Exposes the workflow as a tool for AI employees to call AI-driven business operations

This tutorial uses both Collection event and Custom action event triggers. The other types work similarly; once you’ve learned these, you can pick up the rest quickly.

NocoBase workflows are a built-in plugin — no extra installation needed, ready to use out of the box.

6.2 Scenario 1: Automatically Notify the Assignee on New Tickets

Requirement: When someone creates a new ticket and specifies an assignee, the system automatically sends an in-app message to the assignee, letting them know “there’s work to do.”

Step 1: Create a Workflow

Open the plugin settings menu in the top-right corner and go to Workflow management.

06-workflows-2026-03-14-23-50-45

Click New, and in the dialog that appears:

  • Name: Enter “Notify assignee on new ticket”
  • Trigger type: Select Collection event

06-workflows-2026-03-14-23-53-37

After submitting, click the Configure link in the list to enter the flow editor.

Step 2: Configure the Trigger

Click the trigger card at the top to open its configuration drawer:

  • Collection: Select Main datasource / “Tickets”

  • Trigger on: Select “After record created or updated”

  • Changed fields: Check “Assignee” — the workflow only triggers when the Assignee field changes, avoiding unnecessary notifications from other field updates (when creating a record, all fields are considered changed, so new tickets will also trigger)

  • Only triggers when conditions are met: Set the mode to “Match any condition in the group,” then add two conditions:

    • assignee_id is not empty
    • Assignee / ID is not empty

Why two conditions? Because at trigger time, the form may only have the foreign key (assignee_id) without the associated object loaded, or vice versa. Using OR ensures the workflow triggers as long as an assignee is specified.

  • Preload associations: Check “Assignee” — the notification node needs the assignee’s information, so it must be preloaded in the trigger

06-workflows-2026-03-14-23-58-31

Click Save. The trigger itself now handles the condition filtering — it only fires when the Assignee is not empty, so there’s no need for a separate condition node.

Step 3: Add a Notification Node

Click the + below the trigger and select a Notification node.

06-workflows-2026-03-15-00-00-55

Open the notification node’s configuration. The first option is to select a Notification channel — but we haven’t created one yet, so the dropdown is empty. Let’s go create one first.

06-workflows-2026-03-15-00-10-12

Step 4: Create a Notification Channel

NocoBase supports multiple notification channel types:

Channel Type Description
In-app message Browser notifications, pushed in real-time to the user’s notification center
Email Sends via SMTP, requires email server configuration

For this tutorial, we’ll use the simplest option — In-app message:

  1. Open the plugin settings in the top-right corner and go to Notification management
  2. Click Create channel

06-workflows-2026-03-15-00-13-07

  1. Select In-app message as the channel type, and enter a name (e.g., “System In-App Messages”)
  2. Save

06-workflows-2026-03-15-00-17-55

Step 5: Configure the Notification Node

Go back to the workflow editor and open the notification node’s configuration.

The notification node has the following configuration options:

  • Notification channel: Select the “System In-App Messages” channel you just created
  • Receivers: Click to select Query users → set id = Trigger variable / Trigger data / Assignee / ID
  • Title: Enter a notification title, e.g., “You have a new ticket to handle.” Supports variables — for example, include the ticket title: New ticket: {{Trigger data / Title}}
  • Content: Enter the notification body. You can also insert variables to reference the ticket’s priority, description, and other fields

06-workflows-2026-03-15-20-10-11

(Before leaving the popup for the next step, remember to save first!)

  • Desktop detail page: Enter the URL path of the ticket detail page. To get it: open any ticket’s detail popup in the frontend, then copy the path from the browser address bar — it looks like /admin/camcwbox2uc/view/d8f8e122d37/filterbytk/353072988225540. Paste the path into the field, then replace the number after filterbytk/ with the trigger data’s ID variable (click the variable selector → Trigger data → ID). Once configured, clicking the notification in the list will navigate directly to that ticket’s detail page and automatically mark it as read

06-workflows-2026-03-15-00-28-32

06-workflows-2026-03-15-20-15-19

  • Continue on failure: Optional. If checked, the workflow won’t stop even if the notification fails to send

After the notification is sent, the assignee can see the message in the Notification center (top-right corner of the page). Unread items will show a red dot indicator. Clicking the notification takes you directly to the ticket detail page.

Step 6: Test and Enable

The complete flow for Scenario 1 has just two nodes: Trigger (with condition filtering) → Notification. Simple and direct.

Don’t rush to enable it — workflows provide a manual execution feature that lets you test the flow with specific data first:

  1. Click the Execute button in the top-right corner (not the enable toggle)

  2. Select an existing ticket record as the trigger data

    If the ticket selector shows IDs instead of titles, go to Data sources → Collections → Tickets and set the “Title” column as the title field.
    06-workflows-2026-03-15-19-47-57

  3. Click execute. The workflow will run and automatically switch to a duplicated new version.
    06-workflows-2026-03-15-19-57-19

  4. Click the three dots in the top-right corner and select Execution history. You should see the execution record we just created. Click to view the details — including trigger info, each node’s execution details, and parameters.
    06-workflows-2026-03-15-19-58-34

06-workflows-2026-03-15-20-01-02

  1. The ticket we just tested seems to be assigned to Alice. Let’s switch to Alice’s account — notification received!

06-workflows-2026-03-15-20-16-22

Click the notification to jump to the target ticket page. The notification is automatically marked as read.

06-workflows-2026-03-15-20-16-54

Once confirmed, click the On/Off toggle in the top-right corner to enable the workflow.

06-workflows-2026-03-15-20-18-16

Note: Once a workflow has been executed (including manual execution), it becomes read-only (grayed out) and can no longer be edited. If you need to make changes, click “Duplicate to new version” in the top-right corner and continue editing the new version. The old version is automatically disabled.

06-workflows-2026-03-15-20-19-11

Go back to the tickets page and create a new ticket — make sure to select an assignee. Then switch to the assignee’s account and check the notification center — you should see a new notification.

06-workflows-2026-03-15-20-22-00

Congratulations, your first automation is up and running!

6.3 Scenario 2: Automatically Record Completion Time

Requirement: When a ticket is marked as “Completed,” the system automatically fills in the “Completion time” field with the current time. No manual entry needed, and it never gets forgotten.

If you haven’t created a “Completion time” field in the Tickets collection yet, go to Collection management → Tickets and add a Date type field named “Completion time.” Refer to Chapter 2 for how to create fields — we won’t repeat the steps here.
06-workflows-2026-03-15-20-25-38

Step 1: Create a Workflow

Go back to the workflow management page and click New:

  • Name: Enter “Auto-record ticket completion time”
  • Trigger type: Select Custom action event (fires when a user clicks a button bound to this workflow)
  • Execution mode: Synchronous

About synchronous vs asynchronous:

  • Asynchronous: After the action, you can continue doing other things while the workflow runs in the background
  • Synchronous: After the action, the UI waits for the workflow to finish before you can do anything else

06-workflows-2026-03-19-22-56-34

Step 2: Configure the Trigger

Open the trigger configuration:

  • Collection: Select “Tickets”
  • Execution mode: Select Single record mode (processes only the current ticket each time)

06-workflows-2026-03-19-22-58-21

Step 3: Add a Condition

Unlike the collection event trigger which has built-in conditions, we need to add a condition node ourselves:

06-workflows-2026-03-15-20-39-14

We recommend selecting “Continue on ‘Yes’ and ‘No’ separately” for easier future expansion.

  • Condition: Trigger data → Status does not equal Completed (only incomplete tickets pass through; already completed tickets won’t be processed again)

06-workflows-2026-03-19-22-37-59

Step 4: Add an Update Record Node

On the “Yes” branch of the condition, click + and select an Update record node:

06-workflows-2026-03-15-20-46-22

  • Collection: Select “Tickets”
  • Filter condition: ID equals Trigger data → ID (to ensure only the current ticket gets updated)
  • Field values:

    • Status = Completed
    • Completion time = System variables / System time

06-workflows-2026-03-19-22-39-27

This way, a single node handles both “change status” and “record time” — no need to configure field values on the button separately.

Step 5: Create a “Complete” Action Button

The workflow is configured, but a “Custom action event” needs to be bound to a specific action button to trigger. Let’s create a dedicated “Complete” button in the ticket list’s action column:

  1. Enter UI Editor mode. In the ticket table’s action column, click “+” and select a “Trigger workflow” action button

06-workflows-2026-03-19-22-41-31

  1. Click the button settings and change the title to “Complete”, then select a completion-related icon (e.g., a checkmark icon)

06-workflows-2026-03-19-22-43-39

  1. Configure linkage rules for the button: hide it when the ticket status is already “Completed” (completed tickets don’t need a “Complete” button)
    • Condition: Current data → Status equals Completed
    • Action: Hide

06-workflows-2026-03-15-21-15-29

  1. Open “Bind workflows” in the button settings and select the “Auto-record ticket completion time” workflow we just created

06-workflows-2026-03-19-23-00-53

Step 6: Configure Event Flow Refresh

The button is created, but the table won’t automatically refresh after clicking — users won’t see the status change. We need to configure the button’s event flow to automatically refresh the table after the workflow completes.

  1. Click the second lightning bolt icon (⚡) in the button settings to open the Event flow configuration
  2. Configure the trigger event:
    • Trigger event: Select Click
    • Execution timing: Select After all flows
  3. Click “Append step” and select “Refresh target block”

06-workflows-2026-03-20-16-46-59

  1. Find the ticket table on the current page, open its settings menu, and click “Copy UID” at the bottom. Paste the UID into the refresh step’s target block field

06-workflows-2026-03-20-16-48-39

This way, after clicking the “Complete” button, the table automatically refreshes once the workflow finishes, and users immediately see the status and completion time changes.

Step 7: Enable and Test

Go back to the workflow management page and enable the “Auto-record ticket completion time” workflow.

Then open a ticket with “In Progress” status and click the “Complete” button in the action column. You should see:

  • The ticket’s “Completion time” field is automatically filled with the current time
  • The table refreshes automatically, and the “Complete” button has disappeared for this ticket (linkage rule in effect)

06-workflows-2026-03-15-21-25-11

Convenient, right? This is the second common use case for workflows — automatically updating data. And by using the “Custom action event + Button binding” approach, we’ve created a precise trigger mechanism: the workflow only runs when a specific button is clicked.

6.4 Viewing Execution History

How many times has the workflow run? Were there any errors? NocoBase keeps track of everything.

In the workflow management list, each workflow shows an Execution count link. Click it to see the detailed record of each execution:

  • Execution status: Success (green) or failure (red) — easy to spot at a glance
  • Trigger time: When was it triggered
  • Node details: Click in to see the execution result of each node

06-workflows-2026-03-15-21-25-38

If an execution failed, clicking into the details shows which node had the problem and the specific error message. This is the most important tool for debugging workflows.

06-workflows-2026-03-15-21-36-16

Summary

In this chapter, we created two simple but practical workflows:

  • New ticket notification (Collection event trigger): Automatically notifies the assignee when a ticket is created or reassigned — no more yelling across the office
  • Auto-record completion time (Custom action event trigger): Click “Complete” and the timestamp is filled automatically — no more human oversight

These two workflows demonstrate two different trigger types, and together took less than 10 minutes to configure. The system can already do things automatically. NocoBase supports many more node types (HTTP requests, calculations, loops, etc.), but for getting started, mastering these combos is enough to handle most scenarios.

Next Chapter Preview

The system can do things automatically now, but we’re still missing a “big picture view” — how many tickets are there in total? Which category has the most? How many new tickets per day? In the next chapter, we’ll use chart blocks to build a data dashboard to see everything at a glance.

Related Resources

About NocoBase

NocoBase is the most extensible AI-powered no-code platform.
Total control. Infinite extensibility. AI collaboration.
Enable your team to adapt quickly and cut costs dramatically.
No years of development. No millions wasted.
Deploy NocoBase in minutes — and take control of everything.

Discover NocoBase in 3 Minutes!

👇 Get NocoBase

Homepage

Demo

Documentation

GitHub

Real Docker Containers in Playwright Tests — Zero Boilerplate

You want real infrastructure in your integration tests. You don’t want to write cleanup code. Here is how to get both.

The problem with containers in Playwright today

Testcontainers works great in Node.js, but fitting it into Playwright requires manual lifecycle management:

// the old way — lots of ceremony
let container: StartedTestContainer;

test.beforeAll(async () => {
  container = await new GenericContainer("redis:8")
    .withExposedPorts(6379)
    .start();
});

test.afterAll(async () => {
  await container.stop(); // what if beforeAll threw halfway through?
});

test("my test", async () => {
  const port = container.getMappedPort(6379);
  // ...
});

Problems:

  • If beforeAll fails midway, afterAll still runs and may throw on container.stop() against an undefined value.
  • All tests in the file share one container — isolation suffers.
  • The container setup has nothing to do with what you’re testing, but it’s taking up a third of your file.

The new way

npm install -D @playwright-labs/fixture-testcontainers testcontainers
import { test } from "@playwright-labs/fixture-testcontainers";

test("redis test", async ({ useContainer }) => {
  const container = await useContainer("redis:8", { ports: 6379 });
  const port = container.getMappedPort(6379);
  // container.stop() is called automatically after the test
});

That’s it. One import change, and you get:

  • ✅ Container starts when the test needs it
  • ✅ Container stops after the test ends (even on failure)
  • ✅ Multiple containers tracked and stopped in parallel
  • ✅ Full ContainerOpts support — all GenericContainer.with* methods

Quick examples

Postgres with wait strategy

import { test } from "@playwright-labs/fixture-testcontainers";
import { Wait } from "testcontainers";

test("postgres integration", async ({ useContainer }) => {
  const pg = await useContainer("postgres:16", {
    ports: 5432,
    environment: { POSTGRES_PASSWORD: "secret" },
    waitStrategy: Wait.forLogMessage("ready to accept connections"),
    startupTimeout: 30_000,
  });

  const connStr = `postgresql://postgres:secret@localhost:${pg.getMappedPort(5432)}/postgres`;
  // connect and run queries
});

Multiple containers at once

test("full stack", async ({ useContainer }) => {
  const [redis, pg] = await Promise.all([
    useContainer("redis:8", { ports: 6379 }),
    useContainer("postgres:16", {
      ports: 5432,
      environment: { POSTGRES_PASSWORD: "secret" },
    }),
  ]);
  // both stop in parallel after the test
});

Build from Dockerfile

test("custom service", async ({ useContainerFromDockerFile }) => {
  const app = await useContainerFromDockerFile("./docker", "Dockerfile", {
    ports: 3000,
    waitStrategy: Wait.forHttp("/health", 3000),
  });
});

Compose with your own fixtures

Because useContainer is a Playwright fixture, it plugs directly into your existing fixture chain:

// fixtures.ts
import { test as base } from "@playwright-labs/fixture-testcontainers";

export const test = base.extend<{ redisUrl: string }>({
  redisUrl: async ({ useContainer }, use) => {
    const container = await useContainer("redis:8", { ports: 6379 });
    await use(`redis://${container.getHost()}:${container.getMappedPort(6379)}`);
  },
});

// my.spec.ts — tests never touch Docker at all
import { test } from "./fixtures";

test("cache behavior", async ({ redisUrl }) => {
  // just use the URL
});

Custom matchers

Import expect from the package to unlock container-specific assertions:

import { test, expect } from "@playwright-labs/fixture-testcontainers";

test("container assertions", async ({ useContainer }) => {
  const container = await useContainer("postgres:16", {
    ports: 5432,
    environment: { POSTGRES_PASSWORD: "secret" },
    healthCheck: { test: ["CMD-SHELL", "pg_isready -U postgres"], interval: 1_000, retries: 5 },
    waitStrategy: Wait.forHealthCheck(),
  });

  await expect(container).toBeContainerRunning();
  await expect(container).toBeContainerHealthy();
  expect(container).toBeContainerPort(5432);
  await expect(container).toMatchContainerLogMessage("ready to accept connections");
  expect(container).toMatchContainerPortInRange(5432, { min: 1024 });
});

Full matcher list:

Matcher What it checks
toBeContainerRunning() State.Running === true
toBeContainerStarted() State.Status === "running"
toBeContainerStopped() State.Status === "exited"
toBeContainerHealthy() State.Health.Status === "healthy"
toMatchContainerLogMessage(pattern) Logs contain string or match RegExp
toBeContainerPort(port) Port is exposed and mapped
toMatchContainerPortInRange(port, range?) Mapped port is within bounds
toHaveContainerLabel(key, value?) Label exists (optionally with value)
toHaveContainerName(name) Exact name match
toMatchContainerName(pattern) Name contains / matches RegExp
toHaveContainerNetwork(name) Connected to the network
toHaveContainerUser(user?) Exact user or any non-empty user
toMatchContainerUser(pattern) User contains / matches RegExp

All .not variants are supported.

Requirements

  • @playwright/test >= 1.57.0
  • testcontainers >= 10.0.0
  • Docker (local or CI)

Source: github.com/vitalics/playwright-labs

If you’ve been putting off writing integration tests because of the Docker lifecycle boilerplate, this is the package that removes that excuse. Give it a try and let me know what you think in the comments!

Presentation: Security and Architecture: To Betray One Is To Destroy Both

Shana Dacres-Lawrence explains the complex relationship between security and architecture, identifying three types of “betrayal” – physical, emotional, and trust – that lead to systemic failure. Drawing on real-world incidents like CrowdStrike and Change Healthcare, she shares five defense strategies: open communication, automation, tech integration, validation, and collaborative culture.

By Shana Dacres-Lawrence

Article: Architecting Autonomy at Scale: Raising Teams Without Creating Dependencies

Modern engineering needs a shift from “gates” to “guardrails.” Scale via decentralized architecture that treats teams like adults—building judgment through Socratic coaching, shared platforms, and automated drift detection. Move beyond bottlenecks to an interdependent model where AI governance and ADRs preserve context without killing velocity. Empower autonomy while maintaining alignment.

By Shweta Aggarwal, Ron Klein