> ## Documentation Index
> Fetch the complete documentation index at: https://docs.humalike.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-defined actions

> Declare deeds your own script performs, gated on the facts it reports.

For server developers writing the integration resource: after this page an
NPC on your server can perform a deed only your script knows how to do, and
only once your script has reported the facts that must come first. Beside the
observations it [reports](/ai-npc/integrations/world-events#server-observations),
the actions provider declares actions: the model chooses them like any
catalogue action, HumaLike lets it perform one only once the required facts
have been reported, and your `RunAction` does the deed.

```lua theme={null}
exports.humalike:RegisterProvider('actions', {
    name = 'my_actions', apiVersion = 1, priority = 100,
    SupportedActions = {},
    Namespace = 'srp',
    Observations = {
        item_given = {
            fields = { item = 'string', quantity = 'integer' },
            template = { en = 'the character handed you {quantity} x {item}' },
        },
    },
    Actions = {
        give_map = {
            name = 'Give the treasure map',
            description = 'Hand the player the map to the hidden chest.',
            params = { copies = { type = 'integer', enum = { 1, 2 } } },
            fixed = { item = 'treasure_map' },
            requires = {
                { observation = 'item_given', where = { item = 'amulet' }, consume = true },
            },
            locked_hint = { en = 'Only once the amulet is in your hands.' },
        },
    },
    RunAction = function(action, source, npcCoords, params)
        if action ~= 'give_map' then return false end
        return exports.ox_inventory:AddItem(source, params.item, params.copies or 1)
    end,
})
```

A shopkeeper works the same way with an amount:

```lua theme={null}
sell_pistol = {
    name = 'Sell a pistol',
    description = 'Hand the customer the pistol they paid for.',
    fixed = { weapon = 'WEAPON_PISTOL' },
    requires = {
        { observation = 'item_given', where = { item = 'cash', quantity = { gte = 500 } },
          consume = true },
    },
    locked_hint = { en = 'Five hundred on the counter first.' },
},
```

A refund that can't be exploited:

```lua theme={null}
refund = {
    name = 'Give the cash back',
    description = 'Return the money the customer put on the counter.',
    requires = { { observation = 'item_given', where = { item = 'cash' }, consume = true } },
    params_from = { amount = 'item_given.quantity' },
},
-- RunAction receives { player_id, amount = <what was actually handed over> }
```

Then enable `srp:give_map` on the NPCs that should have it, in the dashboard,
like any other action.

## Stocking an NPC

What an NPC has to give is your script's to set, never the dashboard's:

```lua theme={null}
exports.humalike:SetNpcStock(npcId, { map = 50, bread = 'unlimited' })
```

The call replaces the NPC's whole shelf, so call it again to restock; an item
not listed is one the NPC has none of. The NPC reads its exact counts, a deed
that `uses_stock` locks at zero, and every delivered deed (a shop `deliver`
included) takes its share. Up to 32 items per NPC, each named as your
inventory spells it (`a-z`, `0-9`, `_`, `.`, `-`, at most 48 characters), with
a whole count up to 1,000,000 or `'unlimited'`. An NPC that is not this
server's, or a shelf outside those bounds, is refused and nothing changes.
The dashboard shows the current shelf on the NPC's page, read-only.

What happens: a player who asks for the map before handing over the amulet is
refused in the NPC's own words ("Not until that amulet is in my hand.") and
nothing reaches your script. After your hook reports
`item_given { item = 'amulet' }` the NPC may answer with the tag,
`[srp:give_map copies=2] Here, take it.`, and `RunAction` receives
`('give_map', source, npcCoords, { player_id = 12, copies = 2, item = 'treasure_map' })`.

What fails: a declaration with a mistake is refused at registration, in your
console, with the field at fault --
`[humalike] actions provider from my-adapter rejected: unknown observation in requirement 1 of action give_map`
\-- and nothing is reported. A tag whose values do not fit the declaration
(`copies=3` here) is dropped before it is spoken or remembered, so your script
never sees a value it did not declare.

## A shop counter

For NPCs that sell, declare a `Catalog` instead of one action per product and
HumaLike runs the whole sale:

```lua theme={null}
Catalog = {
    currency = 'cash',        -- the item_given `item` that counts as payment
    payment = 'item_given',   -- the observation your inventory hook reports
    items = {
        water = { price = 5 },
        bread = { price = 3 },
        pistol = { price = 150, limit = { per_player = 1, every_s = 86400 } },
    },
},
-- `deliver` and `refund` are declared for you by the resource with the
-- Catalog; do not list them under Actions. Your RunAction receives them.
RunAction = function(action, source, npcCoords, params)
    if action == 'deliver' then
        -- params.items = { water = 10, pistol = 1 }, total, paid, change, currency.
        -- Hand over everything or nothing: weigh the basket first, take back
        -- what was added if a line fails, and return true only once every
        -- line and the change are in the customer's hands.
        local weight = 0
        for item, quantity in pairs(params.items) do
            weight = weight + exports.ox_inventory:Items(item).weight * quantity
        end
        if not exports.ox_inventory:CanCarryWeight(source, weight) then return false end
        local added = {}
        local function takeBack()
            for _, line in ipairs(added) do
                exports.ox_inventory:RemoveItem(source, line.item, line.quantity)
            end
            return false
        end
        for item, quantity in pairs(params.items) do
            if exports.ox_inventory:AddItem(source, item, quantity) ~= true then return takeBack() end
            added[#added + 1] = { item = item, quantity = quantity }
        end
        if params.change > 0
            and exports.ox_inventory:AddItem(source, params.currency, params.change) ~= true then
            return takeBack()
        end
        return true
    elseif action == 'refund' then
        -- params.amount, currency: exactly what is on the counter.
        return exports.ox_inventory:AddItem(source, params.currency, params.amount) == true
    end
    return false
end
```

An NPC sells whatever of the catalogue your script
[stocked it with](#stocking-an-npc), at these prices. The model takes the order as one tag with every line
(`[srp:order water=10 pistol=1]`); HumaLike checks the lines against the shelf
and the per-item limits, prices them and tells the NPC the total. Payment is
the reported `item_given` of the currency, in whole units: the payment
observation declares `quantity = 'integer'` (a `number` is refused at
registration), and a reported quantity of zero or less buys nothing. Once the money on the counter
covers the total, HumaLike calls `deliver` itself on the NPC's next reply to
that player, with the basket and the change (164 paid on 163 → 1 back),
records the sale in plain words ("you handed over 10 x water for 50 cash, 1
back as change") and takes every line off the shelf. The NPC is told, in that
reply's prompt, that the goods go out with this reply unless it cancels.

Hand over everything or nothing: return `false` when the customer cannot
take it, and nothing is recorded -- order and money stay on the counter, the
NPC is told the hand-over failed and HumaLike tries again on its next reply
to that player. The retry carries the **same invocation id** for as long as
the same order stands: the id is derived from the order (its record and its
basket), not from the money, so a top-up or a tip in between does not change
it and a `RunAction` that did deliver but lost the answer can recognise the
repeat instead of handing over twice. A new `[srp:order ...]` is a new deed
with a new id. A `refund` is identified the same way by the order it cancels
(by the payments themselves when money sits on the counter with no order).
If the shelf ran out in the meantime (another customer took the last unit),
the hand-over is not attempted; the NPC is told and can cancel for a refund.
Underpaid (160) → nothing moves, the NPC is told what is owed.

One counter deed at a time per customer: while a `deliver` or a `refund` for
a player has not answered yet, HumaLike neither retries it nor accepts a
`[srp:cancel_order]` for that player, and the NPC is told the hand-over is
pending -- a slow callback can never pay out twice. Answer `RunAction`
promptly; HumaLike waits up to 5 seconds. An answer that never arrives (a
timeout, a dropped connection, an HTTP 5xx) is sent again at once, up to
three attempts in all, under the **same invocation id** -- for every action,
not only the counter's. Keep the ids you accepted and answer a repeat from
that record, and a lost reply never runs the deed twice. A `false` is an
answer: a rejected deed is not retried automatically.

If the customer backs out, the NPC's `[srp:cancel_order]` calls `refund` with
exactly what is on the counter -- and wins over a pending delivery in the
same reply, wherever in the line it was written, so what the NPC says and
what happens never diverge. A `refund` your script refuses (`false`) is
remembered too: the NPC is told the money did not go back, HumaLike retries
the refund on its next reply to that player, and the goods do not go out
meanwhile -- the sale was called off out loud. A new `[srp:order ...]` while
one is open replaces it; the money on the counter stays and counts towards
the new total. An order the counter refuses (a line not sold, not enough on
the shelf, over a per-player limit) is recorded with its reason, so the NPC
reads why on its next turn instead of quoting a price for a sale it cannot
make. The model never authors a price, a quantity delivered or an amount
returned. A server with its own shop menu skips the conversation:
`exports.humalike:PlaceOrder(npcId, playerId, { water = 2 })` sends the
picked lines, which HumaLike prices the same way against the live shelf; the
NPC then announces the total. A menu order does not replace one the player
has already paid for: while their paid order awaits hand-over it is refused
(recorded as `order_refused` with reason `order_open`), and the NPC says so.
A spoken re-take waits the same way while a hand-over is pending.

Money on the counter never expires: an unspent payment is the player's until
a `deliver` or a `refund` spends it, so a customer who could not take the
goods and comes back hours later still has their money on the counter and
`[srp:cancel_order]` stays open to give it back. Only the order ages out
after an hour (yesterday's basket is not what today's money is for); the
money then sits on the counter with no order, and the NPC is told so. An
observation's `spent` field is HumaLike's own (it names the facts a deed
spent) and cannot be declared or reported. Declare `deliver` and `refund`
bare -- no `params`, `requires`, `limit`, `uses_stock`, `auto` or
`params_from` -- the counter fills them.

The counter's keys are reserved in your namespace: you cannot declare
`order`, `cancel_order`, `order_placed` or `order_refused` as an action or an
observation, nor `deliver`/`refund` as observations, and no key may be both
an action and an observation.

## Declaration

| Field         | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | Shown in the dashboard. Up to 80 characters.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `description` | The line the model reads under its ACTIONS rules. Up to 400 characters, no square brackets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `params`      | Up to 4 values the model fills in when it writes the tag, typed `string`, `integer` or `boolean`; optional `enum` (up to 16 bare words or integers), `required` and `description`. `player_id` is always added by HumaLike.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `fixed`       | Up to 16 values your script decides. Never sent to HumaLike; merged under the model's values before `RunAction`, and always win.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `requires`    | Up to 4 conditions, all of which must hold: a declared observation reported for this NPC and the player it is answering, matching `where` on declared fields — a scalar exactly (`1` and `1.0` are the same number), a bound on a numeric field (`quantity = { gte = 500 }`, `{ lte = 3 }`, or both), or a sum across hand-overs (`quantity = { sum_gte = 2 }`: one bottle and one bottle make two) — within `within_s` seconds (5–3600, default 600). `consume = true` spends, once the deed is done, every fact that condition counted, all at once: an amulet that bought a map is gone, so a second map needs a second amulet (two amulets handed over before asking both go on the one map). Facts are spent by identity across every deed and the shop counter: cash a `consume` deed took is not money on the counter, and money the counter delivered on is gone for every other deed. |
| `locked_hint` | What the NPC is told, per language (`en`, `pl`), while a condition is unmet.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `limit`       | `{ per_player = 1, every_s = 86400, hint = { en = '...' } }`: how many times one player may get the deed in any window of `every_s` seconds (up to 7 days), counted from the deeds HumaLike recorded — a player cannot talk it back. No limit unless declared.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `auto`        | `true`: HumaLike performs the deed as soon as its conditions hold, the next time the NPC answers that player — paid means served, whether or not the model writes the tag. Needs a `consume = true` condition.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `params_from` | `{ amount = 'item_given.quantity' }`: values your script receives from the facts that unlocked the deed, never from the model -- the facts counted by the **first** `requires` entry on that observation, so with two conditions on `item_given` (cash and a voucher) declare the one to read from first. Numbers are summed over those facts, anything else is the newest value, so a refund is for exactly the cash received; the deed cannot be talked up or paid twice.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `uses_stock`  | `{ item = 'map', quantity = 1 }`: the deed takes from the NPC's shelf, which your script [stocks](#stocking-an-npc); the NPC reads its exact counts, the action locks at zero and every delivered deed takes its share.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

Keys are namespaced on the wire (`srp:give_map`) and local in `RunAction`
(`give_map`). Everything is re-declared on every capability report, so a
changed or removed action takes effect on the next resource start; an NPC that
had a removed action enabled simply stops being offered it.

## How the gate works

* A player saying the amulet was handed over is talk. Only the observation your
  script reports counts, and only for the player the NPC is answering.
* While a condition is unmet the action stays listed as something the NPC can
  do, but the tag is not accepted this turn and the NPC is told to say what has
  to happen first. Nothing reaches `RunAction`.
* The check runs again when the model writes the tag, so a fact that expired
  while it was answering does not slip through.
* A delivered deed is written into the NPC's transcript as a world event
  naming the facts it spent, so the NPC remembers doing it and every
  `consume` condition -- and the shop counter -- knows those facts are gone.
* `RunAction` returning `false` marks the invocation rejected; the model may
  write the tag again on its next reply (the per-action cooldown is released),
  and that is a new deed with a new invocation id. Only a lost answer is
  retried under the same id, by HumaLike, at once. Prefer expressing state as
  observations over refusing at run time: by then the NPC has spoken.
* The NPC's gate reads its recent conversation plus, beside it, the newest
  of its server-namespaced world events (your reported facts, its performed
  deeds, the counter's records) however old they are, so a busy NPC forgets
  neither an unpaid order nor a spent fact because a conversation scrolled
  past it. Money on the counter does not expire: an unspent payment is
  refundable until a settlement spends it, and `[srp:cancel_order]` stays
  open while any unspent payment exists; only an order ages out (after an
  hour).

## Debugging

* A refused declaration is printed by `RegisterProvider` in your console with
  the field at fault.
* The dashboard's NPC page lists the server's declared actions with their
  conditions; the transcript view shows the observations and the deeds.
* With `humalike_debug 1`, the resource logs every inbound push
  (`inbound push: npc=… action=srp:give_map`) and its result.

## Next

* [Report the observations these actions depend on](/ai-npc/integrations/world-events#server-observations).
* [Review the provider contract](/ai-npc/integrations/provider-api).
* [Build the adapter from the template](/ai-npc/integrations/custom-bridge).
