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

# Writing scripts with an AI

> The complete authoring contract for Mikting scripts, for models without shell access.

> [!NOTE]
>
> This page restates every rule that `mikting.schema.json` and `tools/mikting-lint.mjs` enforce, so a
> model can write a correct script without the repository, npm, or a terminal. If you do have the
> tools, run `node tools/mikting-lint.mjs --strict <file>` as a second check, but do not depend on it.

## If you are a model, do this in order

1. Pick the `triggers:` value from the trigger table. Every top-level action needs one, unless the
   script is only called as a follow-up, include, or command.
2. Choose actions only from the action table below, and use only the args listed for that action.
   Unknown arg names are rejected at runtime.
3. Put `follow-up-actions` **inside** `args`. It is never a top-level field.
4. Put `not-met-actions` **only** on a condition, never on an action.
5. Declare the capabilities the script uses under `permissions:`.
6. Check the result against [Before you answer](#before-you-answer).

## Where a script lives

| File | Path | Notes |
| - | - | - |
| Script | `configs/Mikting/scripts/<name>.yml` | The filename becomes the `/mikting-debug <name>` subcommand. |
| Library | `configs/Mikting/libs/<name>.yml` | Must declare `version: 1`. |
| Include | any `.yml` inside the bot working directory | Must be an object with an `actions:` list. |
| Shared secrets | `configs/Mikting/scripts/.env` | Loaded at startup, read-only. |

Settings such as `storage:`, `secrets.prefix`, `logging:`, `libs:`, `server:`, `http:`, and `safety:`
belong to the **addon configuration**, not to a script. A script that contains them fails schema
validation, because the root object allows only the keys below.

## Root object

Five keys, and nothing else:

```yaml theme={null}
permissions: [http.egress]   # optional
actions: []                  # required, 0 to 500 items
conditions: []               # optional
debug: {}                    # optional
command: {}                  # optional
```

`actions:` is required even when empty, so an on-demand-only script still needs `actions: []`.

## Rules that get a script rejected

| # | Rule |
| - | - |
| 1 | Root keys are exactly `actions`, `conditions`, `permissions`, `debug`, `command`. |
| 2 | `actions` must be a list of mappings, each with an `id`. At most 500 per file. |
| 3 | `follow-up-actions` must be inside `args`. Top-level `follow-up-actions` is an error. |
| 4 | `not-met-actions` belongs to a condition. On an action, or inside `args`, it is an error. |
| 5 | Only the documented args are accepted per action. Any other key is rejected. |
| 6 | `miktingBranch`, `miktingForEach`, `miktingWhile`, `miktingTry`, `miktingSleep`, `miktingStop`, `miktingCallFunction`, and `miktingInclude` take **no** `follow-up-actions`. Use their nested lists. |
| 7 | `storeAs`, `itemVar`, and `storeErrorAs` must match `^[A-Za-z_][A-Za-z0-9_]*$`. |
| 8 | `miktingHttp` requires `url` and at most one of `json`, `form`, `text`. |
| 9 | `miktingBranch` requires `when:` as a string of at most 512 characters. |
| 10 | `miktingForEach` and `miktingWhile` require `actions:`. `maxIter` is an integer of at least 1. |
| 11 | `miktingSleep` accepts `ms` from 0 to 60000. |
| 12 | `miktingInclude` requires `path` ending in `.yml` or `.yaml`, inside the working directory. |
| 13 | Regex patterns (`pattern`, `matches`, `regex`) must compile, be at most 500 characters, and avoid nested quantifiers such as `(a+)+`. |
| 14 | `expr`, `when`, and `cond` are limited to 512 characters. |
| 15 | A script file is at most 256 KiB. |
| 16 | Never put credentials in YAML. No `sk-…`, `xox…`, `gh…_`, `Bearer <token>`, and no `MIKTING_X=value` assignments. Use `{{secret.NAME}}`. |
| 17 | `command.name` and `command.options[].name` must match `^[a-z0-9-_]{1,32}$`, be unique, and have descriptions of 1 to 100 characters. |

## Text substitution

| Syntax | Resolves to | Where |
| - | - | - |
| `{{secret.NAME}}` | Environment secret `MIKTING_NAME` | Any string arg, resolved first |
| `%placeholder%` | Bot placeholder from the event context | Any string arg |
| `[[variable]]` | A script variable set earlier | Any string arg |

Order matters: secrets, then placeholders, then variables. A missing secret fails the action with
`missing_secret:NAME` and sets `mikting_error`. Strings are interpolated inside URLs, headers, query
values, JSON bodies, form bodies, and text bodies.

## Actions

Args are exhaustive: an unlisted arg name is an error.

### HTTP

| Action | Args |
| - | - |
| `miktingHttp` | `url`, `method` (`GET`,`POST`,`PUT`,`PATCH`,`DELETE`,`HEAD`), `headers`, `query`, `json`, `form`, `text`, `auth`, `timeoutMs`, `retries`, `cacheTtlSec`, `maxBytes`, `userAgent`, `responseCheck`, `policyThreshold`, `storeAs`, `follow-up-actions` |
| `miktingHttpPaginate` | `url`, `method`, `headers`, `query`, `nextPath`, `linkHeader`, `maxPages`, `storeAs`, `follow-up-actions` |
| `miktingModerationCheck` | `policy`, `input`, `mode` (`shadow`,`enforce`), `replyContext`, `queueReview`, `storeAs`, `follow-up-actions` |

`auth` is `{ type: bearer, tokenSecret: NAME }` or `{ type: basic, userSecret: U, passSecret: P }`.
`responseCheck` is one of `openai-moderation`, `openai-policy`, `groq-policy`, `jev-policy`.

`miktingHttp` sets `mikting_status`, `mikting_ok`, `mikting_body`, `mikting_json`, flattened JSON
paths, `mikting_headers_<name>`, and `mikting_error`. With `storeAs: probe`, the same values use the
`probe_` prefix, and a JSON field at `results.0.flagged` becomes `[[probe_json_results_0_flagged]]`.

### Variables

| Action | Args |
| - | - |
| `miktingSet` | `key`, `value`, `follow-up-actions` |
| `miktingGet` | `key`, `storeAs`, `follow-up-actions` |
| `miktingAppend` | `key`, `value`, `sep`, `follow-up-actions` |
| `miktingClear` | `keys`, `follow-up-actions` |
| `miktingMath` | `expr`, `storeAs`, `follow-up-actions` |

`miktingMath` evaluates a filtrex expression, not JavaScript. A missing key sets `mikting_error`.

### Text and data

| Action | Args |
| - | - |
| `miktingString` | `op` (`upper`,`lower`,`trim`,`length`,`slice`,`replace`,`split`,`join`,`regex`), `input`, `pattern`, `replacement`, `from`, `to`, `sep`, `storeAs`, `follow-up-actions` |
| `miktingJson` | `op` (`parse`,`stringify`,`query`), `input`, `path`, `storeAs`, `follow-up-actions` |
| `miktingArray` | `op` (`len`,`first`,`last`,`reverse`,`unique`,`join`,`includes`,`at`), `input`, `item`, `sep`, `index`, `storeAs`, `follow-up-actions` |
| `miktingDate` | `op` (`now`,`format`,`parse`,`add`), `input`, `unit` (`ms`,`s`,`m`,`h`,`d`), `amount`, `storeAs`, `follow-up-actions` |
| `miktingRandom` | `kind` (`int`,`uuid`,`choice`), `min`, `max`, `choices`, `storeAs`, `follow-up-actions` |
| `miktingHash` | `algo` (`sha256`,`md5`), `input`, `storeAs`, `follow-up-actions` |
| `miktingEncode` | `op` (`base64`,`hex`,`url`), `dir` (`encode`,`decode`), `input`, `storeAs`, `follow-up-actions` |

`miktingRandom` with `kind: int` is inclusive and defaults to `min: 0`, `max: 100`. `miktingJson.query`
uses a dot path such as `users.0.name`.

### Flow control

These take no `follow-up-actions`.

| Action | Args |
| - | - |
| `miktingBranch` | `when`, `then`, `else` |
| `miktingForEach` | `list`, `itemVar`, `actions`, `maxIter` |
| `miktingWhile` | `cond`, `actions`, `maxIter` |
| `miktingTry` | `try`, `catch`, `storeErrorAs` |
| `miktingSleep` | `ms` |
| `miktingStop` | none |
| `miktingCallFunction` | `name`, `args`, `storeAs` |
| `miktingInclude` | `path` |

`then`, `else`, `actions`, `try`, and `catch` are action lists. `miktingForEach` defaults to 100
iterations, `miktingWhile` to 25, both clamped to 1000. `miktingCallFunction` copies back only
`result`, under `storeAs` when given.

### Discord

| Action | Args |
| - | - |
| `miktingReply` | `content`, `embeds`, `components`, `ephemeral`, `follow-up-actions` |
| `miktingDm` | `user`, `content`, `embeds`, `components`, `follow-up-actions` |
| `miktingReact` | `value`, `follow-up-actions` |
| `miktingPoll` | `question`, `options`, `durationH`, `follow-up-actions` |
| `miktingTimeout` | `durationSec`, `user`, `reason`, `follow-up-actions` |
| `miktingModerate` | `op`, `user`, `reason`, `deleteMessagesSec`, `role`, `nickname`, `slowmodeSec`, `follow-up-actions` |
| `miktingWebhookSend` | `url`, `content`, `username`, `storeAs`, `follow-up-actions` |
| `miktingPaginate` | `pages`, `timeoutSec`, `follow-up-actions` |
| `miktingWaitFor` | `type`, `timeoutSec`, `fromUser`, `storeAs`, `follow-up-actions` |
| `miktingModerationRespond` | `channel`, `storeAs`, `follow-up-actions` |

`miktingModerate.op` is `ban`, `unban`, `kick`, `addRole`, `removeRole`, `nickname`, or `slowmode`.
`user` defaults to the context member; `slowmode` instead needs a text channel context. Ban caps
`deleteMessagesSec` at 604800, slowmode caps `slowmodeSec` at 21600. `miktingPoll` takes 2 to 10
options and `durationH` from 1 to 336. `miktingWaitFor.type` is `message` or `reaction`.

Polls, voice, scheduled events, and automod need the primary guild context.

### Store

| Action | Args |
| - | - |
| `miktingStoreSet` | `key`, `value`, `scope`, `ttlSec`, `follow-up-actions` |
| `miktingStoreGet` | `key`, `scope`, `storeAs`, `follow-up-actions` |
| `miktingStoreIncr` | `key`, `by`, `scope`, `storeAs`, `follow-up-actions` |
| `miktingStoreListOp` | `op` (`append`,`list`), `key`, `value`, `scope`, `storeAs`, `follow-up-actions` |
| `miktingStoreRemove` | `key`, `scope`, `follow-up-actions` |

`scope` is `global`, `guild`, `user`, `channel`, or `message`. A missing key sets
`mikting_error` to `missing_key:<k>`. `miktingStoreRemove` with `key: '*'` clears the scope. Values
default to a 64 KiB cap.

## Conditions

A condition returns true or false. Prefix its id with `!` to invert it. `not-met-actions` runs when
the result is false and holds an action list.

| Condition | Args |
| - | - |
| `miktingVarCheck` | `key`, one of `equals`, `contains`, `matches` |
| `miktingJsonMatch` | `storeAs`, `path`, one of `equals`, `regex` |
| `miktingHttpOk` | `storeAs` |
| `miktingInVoice` | `channel` |
| `miktingAccountAge` | `days` |
| `miktingJoined` | `days` |
| `miktingIsTimeouted` | none |
| `miktingListIncludes` | `key`, `item` |
| `miktingDateCompare` | `a`, `op` (`before`,`after`,`sameDay`), `b` |
| `miktingExpr` | `expr` |

`miktingExpr` provides `len(x)` and `num(x)` and is evaluated by filtrex, not JavaScript.

## Triggers

| Trigger | Variables |
| - | - |
| `miktingWebhook:<hookId>` | `hook_id`, `hook_body`, `hook_body_<path>` |
| `miktingVoiceJoin`, `miktingVoiceLeave`, `miktingVoiceMove` | `voice_old`, `voice_new`, `voice_old_name`, `voice_new_name` |
| `miktingPollVote` | `poll_answer`, `poll_text`, `poll_voter`, `poll_message` |
| `miktingScheduledEventCreate` | `event_id`, `event_name`, `event_start` |
| `miktingAutomodFlag` | `automod_rule`, `automod_action`, `automod_content` |

Core ItsMyBot triggers such as `messageCreate` also work. HTTP failures set `[[mikting_error]]` and
`[[mikting_ok]] = false`, and follow-ups still run. There is no `miktingHttpError` trigger.

## Placeholders

| Placeholder | Value |
| - | - |
| `%mikting_var_content%` | Trigger message content |
| `%mikting_var_guild_id%`, `%mikting_var_guild_name%` | Guild id or name |
| `%mikting_var_channel_id%` | Channel id |
| `%mikting_last_status%`, `%mikting_last_ok%` | Last HTTP status or success flag |
| `%mikting_store_<key>%` | Global store value, JSON-encoded when not a string |
| `%mikting_hook_url%` | Public webhook base URL |
| `%mikting_libs%` | Number of loaded library functions |
| `%mikting_store_driver%` | `sqlite` or `memory(fallback)` |

## Permissions

Declare the capabilities a script uses. Enforcement is warn-first, but strict lint reports a missing
declaration.

| Action | Requires |
| - | - |
| `miktingHttp`, `miktingHttpPaginate` | `http.egress` |
| `miktingWebhookSend` | `webhook.send`, `http.egress` |
| `miktingStoreSet`, `miktingStoreIncr`, `miktingStoreListOp`, `miktingStoreRemove` | `store.write` |
| `miktingModerate` `ban` or `unban` | `moderate.ban` |
| `miktingModerate` `kick` | `moderate.kick` |
| `miktingModerate` `addRole` or `removeRole` | `role.manage` |
| `miktingModerate` `nickname` | `nickname.manage` |
| `miktingModerate` `slowmode` | `slowmode.manage` |
| `miktingTimeout` | `moderate.timeout` |
| `miktingDm` | `dm.send` |
| `miktingPoll`, `miktingPaginate` | `poll.create` |
| `miktingWaitFor` | `wait.listen` |

## Command block

A script can register a slash command instead of, or in addition to, event actions.

```yaml theme={null}
command:
  mode: standalone           # or subcommand
  name: report               # ^[a-z0-9-_]{1,32}$
  description: 'Report a message to the moderators'
  permission: manageMessages
  options:
    - name: message
      description: 'Message link to report'
      type: string           # string|integer|number|boolean|user|channel|role|mentionable|attachment
      required: true
  actions:
    - id: miktingReply
      args: { content: 'Reported.' }
```

Options are limited to 25 per command and to 25 choices each. Names must be unique and lowercase.

## Debug block

`debug.actions` runs on `/mikting-debug <script-name>`. Root `actions` stay the event path.

```yaml theme={null}
actions: []
debug:
  input: { title: 'API check', label: 'Text to check' }
  response-check: { type: openai-moderation, storeAs: probe }
  actions:
    - id: miktingHttp
      args:
        method: POST
        url: 'https://api.openai.com/v1/moderations'
        auth: { type: bearer, tokenSecret: OPENAI_API_KEY }
        json: { model: omni-moderation-latest, input: '[[debug_input]]' }
        storeAs: probe
```

`debug.input.title` and `label` are 1 to 45 characters, `placeholder` at most 100. `storeAs` in
`response-check` must match the HTTP action's namespace.

## Libraries

```yaml theme={null}
version: 1
functions:
  - name: math.add
    params: [a, b]
    actions:
      - id: miktingMath
        args: { expr: '[[a]] + [[b]]', storeAs: result }
```

Function names match `^[A-Za-z0-9_.-]{1,64}$`, at most 20 params. A file without `version: 1` is
skipped with a warning. Call it with `miktingCallFunction` and `name:`.

## Canonical examples

Minimal event script:

```yaml theme={null}
actions:
  - id: miktingReply
    triggers: messageCreate
    args:
      content: 'Hello [[mikting_var_content]]'
```

HTTP call with failure handling. Note where `follow-up-actions` sits:

```yaml theme={null}
permissions: [http.egress]
actions:
  - id: miktingHttp
    triggers: messageCreate
    args:
      method: POST
      url: 'https://api.example.com/v1/check'
      headers: { Authorization: 'Bearer {{secret.API_TOKEN}}' }
      json: { text: '%message_content%' }
      storeAs: mod
      follow-up-actions:
        - id: miktingBranch
          args:
            when: '[[mod_ok]] == false'
            then:
              - id: miktingReply
                args: { content: 'API error: [[mikting_error]]' }
```

Condition with a failure path:

```yaml theme={null}
actions:
  - id: miktingReply
    triggers: messageCreate
    conditions:
      - id: miktingHttpOk
        args: { storeAs: mod }
        not-met-actions:
          - id: miktingReply
            args: { content: 'Backend is down, try later.' }
    args:
      content: 'Checked.'
```

Counter in the store:

```yaml theme={null}
permissions: [store.write]
actions:
  - id: miktingStoreIncr
    triggers: messageCreate
    args: { key: messages, by: 1, scope: user, storeAs: total }
    conditions:
      - id: miktingExpr
        args: { expr: 'num(total) == 100' }
```

## Wrong and right

| Wrong | Why | Right |
| - | - | - |
| `follow-up-actions:` next to `args` | Actions do not carry it at top level | `args: { follow-up-actions: [...] }` |
| `not-met-actions:` on an action | Conditions only | Put it on a `conditions:` entry under that action |
| `not-met-actions:` inside `args` | Same rule | On the condition object |
| `method: get` | Must be uppercase | `method: GET` |
| `json:` and `form:` together | One body only | Pick one |
| `storeAs: probe-ok` | Hyphen is not a variable character | `storeAs: probe_ok` |
| `follow-up-actions` on `miktingBranch` | Flow actions refuse it | Use `then:` and `else:` |
| `url: 'sk-live-…'` | Secret literal | Keep it in env, reference `{{secret.NAME}}` |
| `storage: { driver: sqlite }` in a script | Broken-out config, not script | Addon configuration |
| Top-level action with no `triggers` | Only valid as a follow-up, include, or command | Add `triggers:` or call it |

## Before you answer

* [ ] Root has `actions:` and nothing outside the five allowed keys.
* [ ] Every action's args appear in that action's row above.
* [ ] `follow-up-actions` appears only inside `args`, and not on the eight flow actions.
* [ ] `not-met-actions` appears only on conditions.
* [ ] Every `storeAs` name matches `^[A-Za-z_][A-Za-z0-9_]*$` and is read with `[[that_name]]`.
* [ ] Each `miktingHttp` has one body (`json`, `form`, or `text`) and a `url`.
* [ ] `miktingBranch.when` is a single-quoted filtrex expression.
* [ ] `permissions:` lists every capability the script touches.
* [ ] No literal credentials and no `MIKTING_*=value` lines.
* [ ] The file is valid YAML: two-space indentation, no tabs, and quotes around values that YAML
  could misread such as `true`, `null`, numbers, or strings containing `:` or `#`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.