> For the complete documentation index, see [llms.txt](https://docs.digia.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.digia.tech/digia-engage/developer-guides/campaign-actions.md).

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

{% hint style="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.
{% endhint %}

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.

***

## 1. 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.

{% hint style="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.
{% endhint %}

### The handler timing contract

A few actions can be owned by your app (see [section 7](#7-app-owned-actions)). 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**.

***

## 2. 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.

***

## 3. 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](#7-app-owned-actions) 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.

{% hint style="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.
{% endhint %}

***

## 4. 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.

***

## 5. 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.

***

## 6. 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.

{% hint style="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.
{% endhint %}

***

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

{% tabs %}
{% tab title="React Native" %}

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

{% endtab %}

{% tab title="Android" %}

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

{% endtab %}

{% tab title="iOS (Swift)" %}

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

{% endtab %}

{% tab title="Flutter" %}

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

{% endtab %}
{% endtabs %}

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.

{% tabs %}
{% tab title="React Native" %}

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

{% endtab %}

{% tab title="Android" %}

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

{% endtab %}

{% tab title="iOS (Swift)" %}

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

{% endtab %}

{% tab title="Flutter" %}

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

{% endtab %}
{% endtabs %}

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.

***

## 8. 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.

{% tabs %}
{% tab title="React Native" %}

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

{% endtab %}

{% tab title="Android" %}

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

{% endtab %}

{% tab title="iOS (Swift)" %}

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

{% endtab %}

{% tab title="Flutter" %}

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

{% endtab %}
{% endtabs %}

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

***

## 9. Request app review

`Action.requestReview` asks the platform to present its in-app review prompt.

{% hint style="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.
{% endhint %}

***

## 10. 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](/digia-engage/start-here/core-concepts.md)
* [Developer Integrations](/digia-engage/developer-guides/developer-integrations.md)
* [User Guides](/digia-engage/user-guides/guides.md)
* [CleverTap Integration](/digia-engage/developer-guides/clevertap.md)
* [MoEngage Integration](/digia-engage/developer-guides/moengage.md)
* [WebEngage Integration](/digia-engage/developer-guides/webengage.md)
