# Campaign Actions

URL: https://www.digia.tech/docs/developer/reference/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.

info
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.
warning
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:

```text
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.
info
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.

info
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

```tsx
await Digia.initialize({
  apiKey: 'YOUR_ACCESS_KEY',
  actionHandlers: {
    customKV: (payload) => handleCampaignCommand(payload),
    deepLink: (url) => appRouter.open(url),
    openURL: (url) => void trustedBrowser.open(url),
  },
});
```

```kotlin
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) },
        ),
    ),
)
```

```swift
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) }
        )
    )
)
```

```dart
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

```tsx
Digia.setCustomKVHandler(customKVHandlerOrNull);
Digia.setDeepLinkHandler(deepLinkHandlerOrNull);
Digia.setOpenURLHandler(openURLHandlerOrNull);
```

```kotlin
Digia.setCustomKVHandler(customKVHandlerOrNull)
Digia.setDeepLinkHandler(deepLinkHandlerOrNull)
Digia.setOpenURLHandler(openURLHandlerOrNull)
```

```swift
Digia.setCustomKVHandler(customKVHandlerOrNil)
Digia.setDeepLinkHandler(deepLinkHandlerOrNil)
Digia.setOpenURLHandler(openURLHandlerOrNil)
```

```dart
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` ). info
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:
```tsx
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:
```tsx
// navigation.ts
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef();
// then: <NavigationContainer ref={navigationRef}> … </NavigationContainer>
```

```tsx
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:
```tsx
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` :
```tsx
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. warning
**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:

```json
{
  "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

```tsx
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,
  },
});
```

```kotlin
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,
        ),
    ),
)
```

```swift
@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) }
        )
    )
)
```

```dart
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 `action` and `version` stable, 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.

warning
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.

## Related pages

- Core Concepts
- Developer Integrations
- User Guides
- CleverTap Integration
- MoEngage Integration
- WebEngage Integration
