Campaign Actions
An action is what happens when a user taps a button, card, or other interactive element in a Digia Engage campaign. A single element can run one action or an ordered list of them.
Actions belong to the Digia Engage core SDK. Whichever CEP delivers the campaign — CleverTap, MoEngage, WebEngage, or a custom plugin — only decides who sees the experience and when. The core SDK decides what the action does inside your app.
Actions behave the same way on every stack — React Native, Android, iOS, and Flutter. Code examples are shown per language for convenience, but the vocabulary, the flow semantics, and the handler contract are identical everywhere.
Campaign authors pick actions in the Digia Engage Dashboard. You never write the wire JSON by hand. The canonical action names on this page are what you'll see when inspecting a campaign payload or an SDK log — they're a reading aid, not something you author.
How an action flow runs
An interactive element carries an ordered list of actions. The SDK runs them one at a time, in order, and moves to the next when the current one completes.
- Order is preserved. Action 1 runs, then action 2, and so on.
- A failure is logged, and the flow continues. If the SDK observes an action fail, it logs it and still runs the remaining actions.
- Variables resolve at tap time. Campaign variables such as
{{ product_id }}are interpolated when the user taps, in URL and deep-link targets, Share and Copy text, and Custom KV keys and values. - Put Hide last. When something must happen before the surface closes, place Hide at the end of the flow.
A missing variable resolves to its configured fallback or an empty string. Treat every interpolated value as untrusted input: validate identifiers and URLs in your handlers before acting on them.
The handler timing contract
A few actions can be owned by your app (see section 7). Your
handler runs as part of the tap's flow, so keep the ordering-sensitive work — navigating,
validating, handing off to a service — in the handler itself. If you kick off
longer-running background work, run it on your own async primitive (a coroutine, Task,
Future, or promise) and own its result and errors. Don't rely on that background work
finishing before the next authored action runs; instead, sequence with Hide last.
Action catalog
Every action a campaign can run, and whether your app can own it:
| Dashboard action | Canonical type | What it does | App handler |
|---|---|---|---|
| Open URL / Deep link | Action.openUrl | Routes to a screen or opens a web target | Optional |
| Hide / Dismiss | Action.dismiss | Closes the current campaign surface | — |
| Next | Action.next | Advances a multi-step experience | — |
| Previous | Action.previous | Returns to the previous step | — |
| Share | Action.share | Opens the system share sheet | — |
| Copy to clipboard | Action.copyToClipBoard | Copies text to the clipboard | — |
| Request app review | Action.requestReview | Asks the store for its in-app review prompt | — |
| Custom KV | Action.customKV | Sends a key–value payload to your app | Required |
The dashboard may emit a surface-specific canonical name for Hide — such as
Action.dismissDialog or Action.hideBottomSheet. These all express the same
"dismiss the current surface" intent.
Open URL and Deep link
Open URL and Deep link are two modes of the same Action.openUrl. The SDK decides which
mode to use from the action's launchMode, not from whether the target starts with
https:// or a custom scheme.
launchMode | Mode | Behavior |
|---|---|---|
platformDefault or omitted | Deep link | Let the system or app route the target |
externalApplication | Open URL | Open in an external app, normally the browser |
inAppBrowser | Open URL | Open in an in-app browser |
A custom-scheme deep link looks like:
yourapp://products/123?source=engage
Deep link is for in-app navigation. Provide a deepLink handler when your app should
own routing; it receives the target string and fully owns the action. Without a handler,
the SDK asks the system to open the target.
Open URL is for web destinations. Provide an openURL handler when your app should
choose how web links are presented; it receives only the URL, so it also chooses the
presentation. Without a handler, the SDK opens the URL with default platform behavior.
See section 7 for how to register these handlers.
Host project setup
Deep links resolve through the native platform, so register your scheme in both host projects regardless of which framework your app is built with:
- Android — add a matching intent filter to
AndroidManifest.xml(custom scheme or App Link). - iOS — add a URL type in
Info.plist, or configure an associated domain for a universal link.
Don't rely on a link action to close the campaign. Add Hide as the last action when the surface should close after navigation.
Hide and Dismiss
Hide closes the current campaign surface — a bottom sheet, dialog, or other dismissible experience. Use it as the final step when the flow must do something else first, such as navigate, share, or run a Custom KV command.
Next and Previous
Next advances a multi-step experience such as a Guide; Previous returns to the preceding step. The experience defines its own boundary behavior — for example, Next on the final step can complete or dismiss the experience.
These actions only make sense on a surface that actually has multiple steps.
Share and Copy to clipboard
Action.share opens the system share sheet with the configured message, after variable
interpolation. Action.copyToClipBoard copies the configured message to the clipboard,
also after interpolation.
The canonical spelling is Action.copyToClipBoard. Older payloads using the copy alias
remain readable, but don't author copy_to_clipboard — it isn't a canonical action name.
App-owned actions
Three actions can be handled by your app instead of by default SDK behavior:
- Deep link — take over in-app navigation.
- Open URL — choose how web links are presented.
- Custom KV — run any app behavior that no built-in action expresses. This action always requires a handler.
A registered handler fully owns its action: the SDK does not run its own built-in behavior afterward. Handlers receive only the link target or the Custom KV payload — no campaign context, and no "suppress default" return value. If the SDK observes a handler fail, it logs the failure and continues with the next authored action.
Register handlers
Register handlers once during initialization.
- React Native
- Android
- iOS (Swift)
- Flutter
await Digia.initialize({
apiKey: 'YOUR_ACCESS_KEY',
actionHandlers: {
customKV: (payload) => handleCampaignCommand(payload),
deepLink: (url) => appRouter.open(url),
openURL: (url) => void trustedBrowser.open(url),
},
});
Digia.initialize(
context = applicationContext,
config = DigiaConfig(
apiKey = "YOUR_ACCESS_KEY",
actionHandlers = DigiaActionHandlers(
customKV = { payload -> handleCampaignCommand(payload) },
deepLink = { url -> appRouter.open(url) },
openURL = { url -> trustedBrowser.open(url) },
),
),
)
try await Digia.initialize(
DigiaConfig(
apiKey: "YOUR_ACCESS_KEY",
actionHandlers: DigiaActionHandlers(
customKV: { payload in handleCampaignCommand(payload) },
deepLink: { url in appRouter.open(url) },
openURL: { url in trustedBrowser.open(url) }
)
)
)
await Digia.initialize(
DigiaConfig(
apiKey: 'YOUR_ACCESS_KEY',
actionHandlers: DigiaActionHandlers(
customKV: (payload) async => handleCampaignCommand(payload),
deepLink: (url) async => appRouter.open(url),
openURL: (url) async => trustedBrowser.open(url),
),
),
);
You can replace or clear any handler at runtime. Passing a null handler restores the SDK's default link behavior. Register a Custom KV handler before publishing a campaign that uses Custom KV, or those taps have nothing to run.
- React Native
- Android
- iOS (Swift)
- Flutter
Digia.setCustomKVHandler(customKVHandlerOrNull);
Digia.setDeepLinkHandler(deepLinkHandlerOrNull);
Digia.setOpenURLHandler(openURLHandlerOrNull);
Digia.setCustomKVHandler(customKVHandlerOrNull)
Digia.setDeepLinkHandler(deepLinkHandlerOrNull)
Digia.setOpenURLHandler(openURLHandlerOrNull)
Digia.setCustomKVHandler(customKVHandlerOrNil)
Digia.setDeepLinkHandler(deepLinkHandlerOrNil)
Digia.setOpenURLHandler(openURLHandlerOrNil)
Digia.setCustomKVHandler(customKVHandlerOrNull);
Digia.setDeepLinkHandler(deepLinkHandlerOrNull);
Digia.setOpenURLHandler(openURLHandlerOrNull);
The same handlers apply across every campaign surface. Register them at app scope so a tap always has a live handler; clear a temporary handler when the screen that owned it goes away.
Deep links in React Native
The deepLink handler receives the link string set on the campaign and navigates inside your
app. It runs at app scope — outside your React component tree — so you cannot use the
useNavigation() hook; you navigate through a module-level handle. Wire it to whatever navigation
library you use (each example below is the function you pass as actionHandlers.deepLink, or later
to Digia.setDeepLinkHandler).
This is in-app navigation — moving around an app that is already running. It is not the OS-level universal link / App Links setup (iOS Associated Domains, Android App Links). You only need those if the same links must also open your app from outside, such as from a push notification, an email, or the browser.
1. Expo Router — the router singleton takes a path string directly, which makes it the
simplest to wire:
import { router } from 'expo-router';
Digia.setDeepLinkHandler((url) => router.push(url)); // url authored as "/product/123"
2. React Navigation (imperative) — navigate through a navigationRef, because the handler
lives outside the component tree:
// navigation.ts
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef();
// then: <NavigationContainer ref={navigationRef}> … </NavigationContainer>
import { navigationRef } from './navigation';
Digia.setDeepLinkHandler((url) => {
if (!navigationRef.isReady()) return; // ignore taps that arrive before the navigator mounts
// Map the campaign's url to a screen + params however your app prefers:
navigationRef.navigate('Product', { id: url.split('/').pop() });
});
3. React Navigation (reuse your linking config) — if your app already resolves URLs
declaratively, convert the string with getStateFromPath instead of hand-mapping each route:
import { getStateFromPath } from '@react-navigation/native';
import { navigationRef } from './navigation';
Digia.setDeepLinkHandler((url) => {
const state = getStateFromPath(url, linking.config); // your existing linking.config, url like "/product/123"
if (state && navigationRef.isReady()) navigationRef.resetRoot(state);
});
4. React Native Linking (OS / external links) — when the target is a real URL scheme or an
https:// universal link, hand it to Linking:
import { Linking } from 'react-native';
Digia.setDeepLinkHandler((url) => void Linking.openURL(url)); // "myapp://product/123" or "https://…"
This last one is also exactly what Digia does when you register no deepLink handler at all —
it runs Linking.openURL, guarded by canOpenURL, falling back to the action's fallback_url if
one is set. So for navigation inside your app, register approach 1 or 2; reach for Linking only
when the target is a genuine OS URL.
Agree the link format with your campaign authors. Whatever they enter in the deep-link action —
/product/123, myapp://product/123, or https://… — your handler must expect that exact shape;
it is case- and format-sensitive. Pick one convention and use it across your campaigns. An optional
fallback_url on the action covers the case where the primary link cannot be opened.
Custom KV
Action.customKV sends a string-to-string payload to your app. It's the supported
extension point for anything the built-in actions don't express. Non-string values in the
payload are discarded during parsing, so design for a flat string map.
Reach for Custom KV to:
- open an app feature that has no stable deep link;
- apply a coupon or select a product in app state;
- start an app-owned workflow such as sign-in, checkout, or support;
- call an app service after validating the payload;
- record a distinct business outcome that campaign lifecycle analytics don't already capture.
For ordinary navigation, prefer Deep link; for web destinations, prefer Open URL. Reserve Custom KV for behavior those two can't express.
Use a versioned command schema
Configure a small, stable payload in the dashboard:
{
"action": "open_product",
"version": "1",
"product_id": "{{ product_id }}",
"source": "engage_campaign"
}
action names an allowlisted command; version lets your app evolve the contract without
guessing which payload shape it received. Keep both stable.
Handle Custom KV in the app
Dispatch on action through an explicit allowlist, validate every field, and ignore
unknown commands and versions.
- React Native
- Android
- iOS (Swift)
- Flutter
import { Digia } from '@digia-engage/core';
function handleCampaignCommand(payload: Record<string, string>): void {
if (payload.version !== '1') return;
switch (payload.action) {
case 'open_product': {
const productId = payload.product_id?.trim();
if (!productId) return;
navigationRef.navigate('ProductDetails', { productId });
return;
}
case 'apply_coupon': {
const coupon = payload.coupon?.trim();
if (!coupon) return;
void cartService.applyCoupon(coupon).catch(reportActionError);
return;
}
default:
console.warn('Unknown Engage action', payload.action);
}
}
await Digia.initialize({
apiKey: 'YOUR_ACCESS_KEY',
actionHandlers: {
customKV: handleCampaignCommand,
},
});
private fun handleCampaignCommand(payload: Map<String, String>) {
if (payload["version"] != "1") return
when (payload["action"]) {
"open_product" -> {
val productId = payload["product_id"]?.trim().orEmpty()
if (productId.isEmpty()) return
appRouter.openProduct(productId)
}
"apply_coupon" -> {
val coupon = payload["coupon"]?.trim().orEmpty()
if (coupon.isEmpty()) return
appScope.launch {
runCatching { cartService.applyCoupon(coupon) }
.onFailure(::reportActionError)
}
}
else -> Log.w("EngageAction", "Unknown action: ${payload["action"]}")
}
}
Digia.initialize(
context = applicationContext,
config = DigiaConfig(
apiKey = "YOUR_ACCESS_KEY",
actionHandlers = DigiaActionHandlers(
customKV = ::handleCampaignCommand,
),
),
)
@MainActor
private func handleCampaignCommand(_ payload: [String: String]) {
guard payload["version"] == "1" else { return }
switch payload["action"] {
case "open_product":
guard let productID = payload["product_id"]?.trimmingCharacters(in: .whitespaces),
!productID.isEmpty else { return }
appRouter.openProduct(productID)
case "apply_coupon":
guard let coupon = payload["coupon"]?.trimmingCharacters(in: .whitespaces),
!coupon.isEmpty else { return }
Task {
do {
try await cartService.applyCoupon(coupon)
} catch {
reportActionError(error)
}
}
default:
print("Unknown Engage action: \(payload["action"] ?? "<missing>")")
}
}
try await Digia.initialize(
DigiaConfig(
apiKey: "YOUR_ACCESS_KEY",
actionHandlers: DigiaActionHandlers(
customKV: { payload in handleCampaignCommand(payload) }
)
)
)
import 'package:flutter/foundation.dart';
import 'package:digia_engage/digia_engage.dart';
Future<void> handleCampaignCommand(Map<String, String> payload) async {
if (payload['version'] != '1') return;
switch (payload['action']) {
case 'open_product':
final productId = payload['product_id']?.trim();
if (productId == null || productId.isEmpty) return;
await navigatorKey.currentState?.pushNamed(
'/product',
arguments: productId,
);
return;
case 'apply_coupon':
final coupon = payload['coupon']?.trim();
if (coupon == null || coupon.isEmpty) return;
await cartService.applyCoupon(coupon);
return;
default:
debugPrint('Unknown Engage action: ${payload['action']}');
}
}
await Digia.initialize(
DigiaConfig(
apiKey: 'YOUR_ACCESS_KEY',
actionHandlers: DigiaActionHandlers(
customKV: handleCampaignCommand,
),
),
);
Custom KV best practices
- Allowlist commands. Dispatch through an explicit table or switch. Never use payload data to select arbitrary code, classes, methods, routes, or URLs.
- Version the contract. Keep
actionandversionstable, and reject unknown versions safely. - Validate every field. Treat campaign data and interpolated variables as external input.
- Keep payloads small and string-only. Pass identifiers and intent, not whole domain objects.
- Never include secrets or sensitive personal data. Payloads can surface in dashboards, logs, and diagnostic reports.
- Make destructive commands safe to repeat. Debounce repeated taps and use idempotency for charges, submissions, or mutations.
- Own recovery. A registered handler suppresses SDK fallback, so log failures and present any user-facing recovery yourself.
- Put Hide last. Validate or start your work before the surface dismisses.
- Avoid duplicate analytics. Digia Engage already records campaign lifecycle events. Emit an app event only for a distinct business outcome.
Request app review
Action.requestReview asks the platform to present its in-app review prompt.
The platform controls eligibility and quota, and returns no rating or completion result. The SDK cannot guarantee that a prompt appeared. Treat it as best-effort — never make business-critical behavior depend on the prompt being shown.
Test before publishing
Test an action-heavy campaign on every target surface your app uses.
- Verify each authored action produces the expected result.
- Test deep links from a cold start and while the app is already active.
- Confirm missing or malformed variables fail safely.
- Verify Open URL and Deep link behavior both with and without your handlers.
- Confirm Custom KV ignores unknown commands and versions.
- Ensure repeated taps can't repeat a destructive operation.
- Verify Hide runs at the intended point in the flow.
- Treat Request app review as best-effort, not a guaranteed prompt.