> For the complete documentation index, see [llms.txt](https://sdk.metamarket.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sdk.metamarket.tech/evaluate-and-integrate/events-and-directives.md).

# Sending Actions

Send a player action, then handle the response, in this order: [what to send](#building-the-request), [which lane it runs on](#which-lane-an-action-runs-on), [why a non-zero skipped is a failure](#treat-a-non-zero-skipped-as-a-failure), [how to read back the effects, mutations, and directives](#reading-the-response) that come with it, and [how a reveal card works](#reveal-showing-you-got-x).

The examples below use `Game.Sdk`, the external path, where you get `IEventClient` from the app facade.

## Building the request

An action is an `ActionRequestDto`:

```csharp
public record ActionRequestDto(
    Guid PlayerId,
    string ActionType,
    long Value,
    string ClientKey,
    DateTimeOffset Timestamp,
    string? AppId = null,
    string? EntityId = null
);
```

| Field        | What you put there                                                                                                                                                                                                                                                                                                                                              |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PlayerId`   | The player this action is for, within your app's own scope. The call is authorized as the caller (the gateway forwarding your game's events, or a first-party module); the platform records the action as your game reports it. See [HTTP API](/evaluate-and-integrate/api-guide.md#what-the-event-endpoints-authenticate).                                     |
| `ActionType` | An `id` from the app's `allowedActions` config: a raw verb (`tap`, `nurse`) or a mechanic (`do_spin`). Not free text; an undeclared type is not processed and is counted in the response's `skipped`. (Quota-gated care actions like `feed` are pool items called through `/use`, not raw actions; see [Worked Examples](/evaluate-and-integrate/examples.md).) |
| `Value`      | Quantity/magnitude for the action (stat increment amount, mechanic multiplier).                                                                                                                                                                                                                                                                                 |
| `ClientKey`  | Your own idempotency token for this call. Required even under `idempotency: none` config (some strategies build their own key, but the field is always present on the wire).                                                                                                                                                                                    |
| `Timestamp`  | Client-observed time of the action.                                                                                                                                                                                                                                                                                                                             |
| `AppId`      | Leave `null` in the DTO you construct. The acting app is set server-side from the gateway credential and the `X-App-Id` header, not from this body field; the engine does not trust a body-supplied app id. The SDK may populate it for its own context, but it is not the source of truth on the wire.                                                         |
| `EntityId`   | Required for `scope: target` actions (raw verbs on a specific game entity, e.g. "feed pet A" vs "feed pet B"); omit for `scope: root`/`none` actions, which the server resolves to the app's root entity. Omitting it on a `target` action returns 400 `target_entity_required`.                                                                                |

You batch several actions in one call: both endpoints take `List<ActionRequestDto>`.

## Which lane an action runs on

`IEventClient` exposes one method per lane:

```csharp
Task<ApiResponse<ClientResponse<ActionBatchResult>>> TrackActionsAsync(List<ActionRequestDto> actions);
Task<ApiResponse<ClientResponse<ActionExecuteResultContract>>> ExecuteActionsAsync(List<ActionRequestDto> actions);
```

The lane is fixed by the action's own config, `ActionContractDto.Output.RequireReceipt`. You don't choose it per call. If `require_receipt` is `true` for that `actionType`, call `ExecuteActionsAsync`; if `false`, call `TrackActionsAsync`. There is no server-side redirect between the two.

## Treat a non-zero skipped as a failure

An action that the engine does not accept is not an error on the wire: the call returns HTTP 200, the action is counted in the response's `skipped` rather than `processed`, and a warning is logged server-side. Transport success is not business success. Treat any non-zero `skipped` as an integration failure. Compare `skipped` against the number of actions you sent, and do not report a batch as processed until it is `0`.

Several conditions produce a skip, and they do not all mean the same thing:

* the `actionType` is not declared in your `allowedActions`;
* it was sent on the wrong lane (a `require_receipt: true` action reached `/track`, or the reverse);
* the app configuration could not be loaded;
* a value rule declared on the action rejected the value;
* the acting entity failed its ownership check;
* the action deduplicated, because its client key was already used within the configured window;
* the action threw while executing, which includes a business-rule rejection and a transient fault such as a datastore error.

What to do depends on which group it falls into:

* **Causes one to five** are configuration or wiring mistakes. An unchanged retry skips again.
* **Cause six**, a duplicate client key, means the work already happened. Do not resend.
* **Cause seven** may be transient. Read state to find out whether the effect landed.

The response gives you a count and no per-action reason, so working out which actions were skipped is your job. Send batches you can reconstruct, keep the client keys you sent, and read player state to establish which effects landed. A batch of one removes the ambiguity entirely and is worth it for any action whose acceptance matters.

Do not resend the batch to find the offender. The actions accepted the first time would be applied again, and one configured `idempotency: none` has no deduplication to stop a double-apply.

Once you know which actions failed, fix the configuration or the lane and resend only those, reading player state first for any that does not deduplicate. If you cannot resolve it, stop and report it as an integration issue rather than treating the batch as done. A production client should surface a non-zero `skipped` the same way it surfaces an error.

Validate `Skipped` **before** running any directives the call returned. The chained `.ProcessDirectivesAsync(...)` helper executes those directives before it hands the result back, and a directive can fire a follow-up action, so a dropped action must stop the flow before that point. Call the raw method, check `Skipped`, then process directives only when it is `0`:

```csharp
var trackResp = await eventClient
    .TrackActionsAsync([new ActionRequestDto(playerId, "tap", 1, clientKey, DateTimeOffset.UtcNow)]);

if (!trackResp.IsSuccess || trackResp.Data is not { } track) { return; } // transport failure
if (track.Data is { Skipped: > 0 })
{
    // Integration failure: an action was dropped despite HTTP 200. Stop here; do NOT run the
    // directives it returned or fold its effects. Fix the action id or its lane, then resend.
    return;
}

// Only after Skipped == 0: fold effects, then mutations, then run directives. This order matters
// (see "Manual fold-then-dispatch" below): a refresh_slot directive must run after the mutations
// fold, or a stale mutation could re-add content the refresh just removed.
effectsMessenger.Publish(track.Effects ?? []);
stateMessenger.Publish(track.Mutations ?? []);
await directiveProcessor.ExecuteAsync(track.Directives, ctx);
```

`track.Data` is `ActionBatchResult { Processed, Skipped }`, no fulfillment detail; an `/execute` call's `Data` is `ActionExecuteResultContract { Processed, Skipped, Receipts }`, one `ActionReceiptContract { ActionType, Items }` per action, each item a `{ Type, Key, Value }` triple describing what the fulfillment moved. The one-line `.ProcessDirectivesAsync(...)` chain shown elsewhere is convenient, but it does not turn a non-zero `Skipped` into a failure and it runs directives first. Use it only where a skip does not need detecting at the call site (you confirm the outcome another way); for any action whose acceptance matters, validate `Skipped` first, as here.

## Reading the response

Both calls return the same four-channel envelope (`ClientResponse<T>`). As an integrator you have two ways to consume it:

### One-liner unwrap: `ProcessDirectivesAsync`

Chain the extension method directly onto the client call, as in the snippets above. It runs the envelope's `Directives` through your `IDirectiveProcessor`, then returns a plain `ApiResponse<T>` whose `.Data` is the domain result and whose `.Effects` / `.Mutations` carry the two state channels for you to read or forward. This is the simplest pattern when you have no persistent view-model to fold into, for example, printing the effects straight to a log or test transcript.

### Manual fold-then-dispatch: for a stateful ViewModel

If you're maintaining a live client VM (vitals bar, slot contents) across many actions, call the raw `IEventClient` method yourself, then apply the three side-channels in this order:

```csharp
var resp = await eventClient.ExecuteActionsAsync(actions);
if (!resp.IsSuccess || resp.Data is not { } env) { return; } // transport failure
if (env.Data is { Skipped: > 0 })
{
    // HTTP 200 is not business success: an action was dropped. Stop before folding state or
    // running directives; surface it as an integration failure and do not treat the batch as done.
    return;
}

// Only after Skipped == 0: fold the state channels, then dispatch directives.
effectsMessenger.Publish(env.Effects ?? []);
stateMessenger.Publish(env.Mutations ?? []);
await directiveProcessor.ExecuteAsync(env.Directives, ctx);
```

Order matters here and is not automatic: `refresh_slot` (an imperative saying "this slot's whole cartridge changed, re-fetch it") must run after the upsert-only `mutations` fold, or a stale mutation for the same slot could re-add an item the refresh just removed. `ProcessDirectivesAsync` behaves differently: it runs directives *before* handing you `Effects`/`Mutations` on the unwrapped result, because it has no messenger of its own to order against. If you need the fold-order guarantee, use the manual pattern, not the one-liner.

`IEffectsMessenger` and `IStateMessenger` are in-process pub/sub: any part of the UI can subscribe to the scale or entity keys it owns, independent of which screen triggered the action. Both are typically registered as singletons for the app's lifetime.

## Reveal: showing "you got X"

`reveal` is a `directives` entry, not a separate field on the response. Whether an `/execute` action produces one depends on `ActionContractDto.Output.Reveal` in that action's config: a silent action (a tap that grants XP) reports only through `effects`; a chance/reward action (a spin) is configured `reveal: true` and its fulfillment receipt is packed into a `reveal` directive (filtered to player-visible items only). `/track` never reveals. Reveal is gated to the `/execute` lane server-side.

You don't special-case `reveal` at the call site. The client SDK ships `RevealDirectiveHandler`, registered against `DirectiveConstants.Types.Reveal`, which deserializes the directive's `items` and calls your `IRevealPresenter.PresentAsync(items)`. Implement `IRevealPresenter` once per host (desktop overlay, test-tool transcript print, or the built-in log default) and register it after your SDK's DI extension; every `reveal` directive from any action or package-open then reaches it automatically through the same `ProcessDirectivesAsync`/`ExecuteAsync` path you already call for everything else.

```mermaid
sequenceDiagram
    participant App as Your code
    participant SDK as IEventClient (Game.Sdk / Platform.Sdk)
    participant Proc as IDirectiveProcessor
    participant Rev as IRevealPresenter

    App->>SDK: ExecuteActionsAsync([action])
    SDK-->>App: ClientResponse{data, effects, mutations, directives}
    App->>Proc: ExecuteAsync(directives, ctx)
    Proc->>Rev: PresentAsync(items)   (only if a reveal directive is present)
    App->>App: fold effects / mutations into VM
```
