◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-16Intermediate

Notifications You Can Finish Without Opening the App — Interactive Notification Actions for Rork Apps

Those buttons and text fields that appear when you long-press a notification. Here is how to implement interactive notification actions in a Rork-built Expo app for an experience that completes without launching, including the background-execution pitfalls.

Rork611Notifications3Expo234Notification ActionsUX Design9React Native242

✦ Premium Article

When I started sending a "word for today" notification in a wellness app, something occurred to me. The user taps the notification, opens the app, reads the message, and closes it again. They were launching the entire app for nothing more than that. If it's just to read, wouldn't it be kinder to let it complete inside the notification itself? That thought is what led me to interactive notification actions.

Interactive notification actions are the buttons and text-input fields that appear when you long-press (or swipe down on) a notification. It's the mechanism behind a "Done / Later" button on a reminder, or a reply field on a message. Because the user can finish the action right there without opening the app, the number of steps drops dramatically.

Rork tends to generate the basic code for sending and receiving notifications, but often not the action definitions or background handling. Here is the design for adding this to an Expo (React Native) app, drawn from my experience as an indie developer shipping apps on the App Store and Google Play.

Define actions grouped into "categories"

Notification actions aren't defined as loose individual buttons; they're grouped into a unit called a "category." For example, you bundle "Done" and "Remind in 1 hour" into a "reminder" category, then specify that category when you send the notification.

In Expo you define them with setNotificationCategoryAsync from expo-notifications.

// notificationCategories.ts — register once at app launch
import * as Notifications from "expo-notifications";
 
export async function registerCategories() {
  await Notifications.setNotificationCategoryAsync("reminder", [
    {
      identifier: "complete",
      buttonTitle: "Done",
      options: { opensAppToForeground: false }, // handle in background, no launch
    },
    {
      identifier: "snooze",
      buttonTitle: "Remind in 1 hour",
      options: { opensAppToForeground: false },
    },
  ]);
 
  await Notifications.setNotificationCategoryAsync("daily_word", [
    {
      identifier: "reply",
      buttonTitle: "Write a reflection",
      textInput: {
        submitButtonTitle: "Send",
        placeholder: "A word about how you feel…",
      },
      options: { opensAppToForeground: true }, // save in-app after input
    },
  ]);
}

The value of opensAppToForeground is the crux of this design. Set it to false and the work runs without bringing the app to the front; set it to true and the app opens after the action. The basic split: false for instant work like a "Done" button, true for work like text input that you'll want to show a screen for afterward.

On the sending side, specify the matching category ID in categoryIdentifier.

await Notifications.scheduleNotificationAsync({
  content: {
    title: "Time to water",
    body: "Give your houseplant a drink",
    categoryIdentifier: "reminder", // the category defined above
  },
  trigger: { hour: 9, minute: 0, repeats: true },
});

Receive the action and branch the handling

The result of a button press arrives via addNotificationResponseReceivedListener. Which action was pressed is in actionIdentifier, and the text-input contents are in userText.

// notificationHandler.ts
import * as Notifications from "expo-notifications";
 
export function listenForActions() {
  return Notifications.addNotificationResponseReceivedListener(async (res) => {
    const action = res.actionIdentifier;
    const data = res.notification.request.content.data;
 
    if (action === "complete") {
      await markReminderDone(data.reminderId);
    } else if (action === "snooze") {
      await Notifications.scheduleNotificationAsync({
        content: { title: "Reminder", body: data.body, categoryIdentifier: "reminder" },
        trigger: { seconds: 3600 },
      });
    } else if (action === "reply") {
      await saveReflection(res.userText ?? ""); // text input arrives in userText
    }
  });
}

Watch out: actionIdentifier also carries OS-reserved special values. A plain tap on the notification body delivers Notifications.DEFAULT_ACTION_IDENTIFIER. If you forget to branch — handling only your own actions and routing the default tap to normal navigation — body taps go unresponsive. I shipped exactly that bug once: "tapping the notification normally does nothing."

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Defining button and text-input actions grouped by category
✦Handling the OS execution-time limit when completing work in the background without launching
✦The action-design criteria that kept my notification opt-in rate from dropping — with measured results
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-07-30
What Renovate may bump in an Expo app, and what it must never touch
Turning on automated dependency updates in a Rork-generated app also hands Renovate the 123 packages Expo SDK 57 pins. Measured on 2026-07-30, six of them sit a full major version behind npm latest. Here is how to generate the ignore list from the SDK instead of maintaining it by hand.