# About Nighty

## Welcome!

### **What is Nighty, and how do I set it up?** <a href="#e89c057a-3018-429e-9ac5-215a35b9b5c6" id="e89c057a-3018-429e-9ac5-215a35b9b5c6"></a>

Nighty is a specialised software tool designed to enhance user experience on Discord. It operates as an advanced bot for a user account, offering users the ability to automate various actions and perform tasks within the Discord platform.

With Nighty, users can enjoy features such as automated message sending, reaction triggers based on specific events or commands, efficient server and channel management, and seamless interaction with external APIs or websites. Nighty serves as a powerful tool to streamline and enhance user interactions within Discord, providing a range of automated functionalities tailored to individual preferences and needs.

To set Nighty up, you can follow the following written steps by our team.


# First Launch

Getting started is a breeze. Follow our simple guide to launch Nighty for the first time!

## Purchase Nighty Key

Purchase a license key at <https://nighty.one/#buy> through our official shop or from one of our authorized resellers.

{% hint style="info" %}
To ensure maximum security, it is imperative to save your Nighty key in a safe and protected location. We recommend storing it in a text file (.txt) on a USB or flash drive, or using a dedicated app on your phone's notes for added convenience. Nevertheless, we strongly discourage the use of pen and paper to safeguard your Nighty key, as this approach may lead to potential loss or unauthorised access. Prioritising the safety of your Nighty key is of paramount importance to maintain the integrity of your Nighty account and its associated functionalities.
{% endhint %}

{% hint style="warning" %}
Each license key is intended for a single owner. Official license keys can be purchased from our authorized shop at nighty.one. Reselling keys unofficially is strictly prohibited and may result in key revocation, ensuring a fair and secure environment for all users.
{% endhint %}

***

## Download the launcher

Download the launcher from our official website at <https://nighty.one/get-links>. You will be asked for a license key before downloading.

***

## Unzipping and Placing the launcher

Unzip the ***Nighty.exe*** from *Nighty.zip* and run the executable.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FYwXAZIl7y473HVyIMB7n%2Funzip1.png?alt=media&amp;token=9a1cb2e1-0f9d-40da-83f0-cd9381ba8522" alt=""><figcaption><p>Unzip (extract) Nighty.exe from Nighty.zip</p></figcaption></figure>

***

## Inputting Your License Key

Nighty will prompt you to input your license key upon startup. If you have previously run Nighty on this computer, it will not prompt you again. Simply enter your license key in the provided field and click "Continue."

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FUNgaSshrQdlb2LcMteGf%2Fkey-input-new.webp?alt=media&amp;token=b742af3f-5e17-42b4-acd6-840c6cd9f69f" alt=""><figcaption><p>License Key Window</p></figcaption></figure>

***

## Connect Nighty to your Discord account

To connect to your account, Nighty offers three login options: ~~**Client Login**~~**, Web Login and Token Login**.

{% tabs %}
{% tab title=" 🔥  Web Login" %}
This option prompts a standard Discord login window, where you can input your Discord credentials or scan a QR code.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FHTBCoWY9cn1dkc8SKMWH%2Flogin2.png?alt=media&amp;token=04153b8e-e863-452c-bfa6-4e7b24f519ce" alt=""><figcaption><p>Web login window</p></figcaption></figure>
{% endtab %}

{% tab title=" Token Login" %}
This method requires providing your account token and password. Check our guide on how to obtain your token below.

How to get your Discord token?

1. Login to Discord in a browser.
2. Open developer tools (F12).
3. Open console tab in the developer tools.
4. Paste the code from the codeblock below into the console and hit enter. If you get a pasting error, type in "allow pasting" and click Enter.
5. Copy your token and input into Nighty.

```
let token;
webpackChunkdiscord_app.push([
  [Math.random()],
  {},
  (r) => {
    for (let m in r.c) {
      try {
        const mod = r.c[m].exports;
        if (mod && mod.default && typeof mod.default.getToken === 'function') {
          token = mod.default.getToken();
          break;
        }
      } catch {}
    }
  },
]);
console.log(token);
```

{% endtab %}
{% endtabs %}

***

## Creating an App

Nighty relies on **user apps** to execute commands on Discord. During the initial setup, you'll be prompted to create one. Simply enter a name of your choice and click **Create** to proceed. You can change the app name anytime later using the `/appedit` command.\\

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FMHtr5JJgDSvh0rwyuSYA%2FScreenshot%20(242).png?alt=media&amp;token=4883cb9a-edbd-4973-a3e9-d23ab4b71a71" alt=""><figcaption><p>Give your App a custom name</p></figcaption></figure>

***

## **Getting Your Application Token**

Nighty now needs to retrieve your application token. **Do not enter anything manually.** Simply click the **"Get Your Application Token"** button and wait for the window to open.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F8Zo0OdUrptap9qZVlArq%2FScreenshot%20(243).png?alt=media&amp;token=55159395-5cf9-413c-9ae6-80529a940335" alt=""><figcaption></figcaption></figure>

Now, simply click the **"Reset Token"** button. You may be asked to confirm this by entering your Discord password or 2FA code - do so if prompted. Once completed, wait a few seconds for Nighty to retrieve your application token.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FnrKvdH7kGG3ByKOVOcUw%2FScreenshot%20(244)%20copy.png?alt=media&amp;token=507dd14e-447b-46c6-8a19-f34bd966a26e" alt=""><figcaption></figcaption></figure>

***

## **Authorize Nighty App**

Almost done! Now, authorize the Nighty app by clicking the **"Authorize"** button.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Fu3qkFgNYDLwJWeMW2IQv%2FScreenshot%20(244)%20aacopy.png?alt=media&amp;token=3085c716-e1e2-4ac8-a3fa-545dd1c9e100" alt=""><figcaption></figcaption></figure>

***

## Done!

Congratulations! Nighty is now successfully connected to your account. For any assistance, please refer to our troubleshooting guide or reach out to our dedicated [support team](https://nighty.support/).

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F17RZAi98CibyfNNmFRtB%2F1.webp?alt=media&amp;token=d736e6be-4ca8-4097-b2ea-70e349aa11c7" alt=""><figcaption></figcaption></figure>


# Usage Guide

This page is your go-to resource for navigating and utilizing Nighty effectively.

Let's walk through the initial steps, ensuring a smooth introduction to the world of Nighty on Discord.

Assuming you've successfully connected Nighty to your Discord account, let's delve into the next steps. If you haven't done so yet, be sure to check out our [First Launch](/getting-started/first-launch) guide for a seamless setup experience. Now, let's explore how to make the most of Nighty's features.

***

## Start with `/help`

{% hint style="info" %}
If you can't see commands, reload Discord with CTRL + R or restart the Discord app.
{% endhint %}

In any server, direct message, or group chat, start your Nighty journey with **`/help`**. Nighty uses slash commands, providing full autocompletion on Discord. This command serves as your gateway, displaying Nighty's main commands, neatly categorized for easy navigation.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FnxfB0ayKxlkSVBHZxn7U%2Fasd123png.png?alt=media&amp;token=9269e69e-d788-42af-b604-c1b71daca4f9" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FRWoaksukQOM6oBJ10cs0%2Fadfdf.png?alt=media&amp;token=78e7f670-94f4-43fd-a689-6aaebe876d27" alt=""><figcaption></figcaption></figure>

***

> Please note that this guide is a work in progress, and additional content will be seamlessly integrated soon.

<br>

<br>


# Changelog

This version is a full recode & revisitation of our previous version (2.2), to bring major performance upgrades and new features.

* Full bot recode
* Updated discord.py-self to latest (2.0 -> 2.1)
* Updated config & data location from the current directory to `%appdata%/Nighty Selfbot`
  * This allows Nighty to run from anywhere (fully portable) as well as without administrator
  * A new startup from scratch is required (previous config files are not compatible with this version)
  * Removed password from config
  * Other values that were in config.json have been moved to their appropriate files in the data folder
* To increase efficiency and reduce the amount of "useless" commands, we decided to merge the majority of our commands
  * Here are a few examples (full command changes at commands list - coming soon, for now, use ctrl + f on the app or .help):
    * Instead of the .customize category, which used to contain commands such as ctitle, ctitlecolor, cimageurl, cdescription, embedcolor, wfooter, wtitle, ... Have all been removed and merged into 1 single command:
      * `.theme <text/embed/webhook> <setting> <"value">`
      * Example usage: `.theme embed title "my new embed title"`
    * Instead of the .notifications category, which used to contain commands such as roleupdates, nickupdates, friendevents, ... Have all been removed and merged into 1 single command:
      * `.notifications <event> <on/off>`&#x20;
      * Example usage: `.notifications ghostpings on`
    * &#x20;The same goes for toastnotifications, soundnotifications, webhooksettings, stop commands (stopforcenick, stopmimic, stopreactuser, stopdeleteannoy, ...), and many more
* Revisited our remote command executor:
  * Now being done seamlessly, as if you were invoking it in the channel
  * Remote UI Script available in Community scripts, to execute commands within the UI instead of sending them in a chat
* Revisited text mode
  * text mode has been made more customizable than ever, allowing you to change its entire layout and markup for every aspect of the bot (title, prefix, command name, command description, body, footer, ...)
* New mode: silent (`.mode silent`)
  * Execute commands without leaving any message behind
  * Instead of sending as a message, it will get sent as a notification within the app
  * This works for any messages being sent by the bot and works within scripts as well
* Redesigned startup:
  * New, quick, and visually pleasing
  * The entire setup is now within one window
  * No restarts between the processes (license key, discord login, ..)
* Command sorting (`.commandsort <default/history/favorites/alphabetic>`)
  * Sort your commands according to your preference:
    * `default`: Default command sorting
    * `history`: Show last used commands first
    * `favorites`: Show favorite commands first
    * `alphabetic`: Sort commands alphabetically
* Commands per page (`.commandsperpage <amount>`)
  * Decide how many commands are displayed per output
* Delete timer seconds are now optional, no seconds given = no delete
  * `.deletetimer [seconds]`
* New value for session spoofing (`.sessionspoof <windows/linux/ios/android/ps5>`):
  * **ps5**: Shows you online on Console
* **Recovery**:
  * Friends & Servers restore:
    * Due to Discord limitations (captcha & other annoying checks), the process is semi-automated and requires user interaction, such as accepting invites & clicking on "Send Friend Request"
    * Requires the Discord client
  * Backup & restore favorited GIFs:
    * `.backupgifs` & `.restoregifs`
* Added back detailed explanations for each command using .help for text & embed mode
* Improved repeat command (`.repeat [channel]`)
  * Directly invokes the previous command instead of resending it as a message
  * Optional channel argument to repeat the command in a different channel
* Completely reworked **sharing commands**:
  * Shared commands work on a 1-1 endpoint
  * Seamlessly and intuitively, as if you were the one invoking them.
* DM logger
  * Fixed false positives for message edits when they include embeds or attachments
* Persistent snipe & able to snipe more than one message
* Removed `.customactivity` -> Now RPC
* Improved `.autoslash`
* Re-coded `.cloneserver`:
  * Clones the following:
    * **text channels**: name, topic, NSFW, slow-mode, position, channel-specific permissions
    * **voice channels**: name, position, channel-specific perms, afk-channel
    * **roles**: name, permissions, color, separated display, mentionable
    * **emojis**
* **Rich Presence**:
  * The RPC has been getting major changes, for now, these changes have been made in the back-end and will be applied to the UI very soon
  * New values (rpc.json):
    * **stream\_url**: Stream URL for the rpc profile (has to be a Twitch or YouTube URL)
    * **start & end**: Custom start & end timer
      * **start**: \[hours, minutes, days] (for example: \[0, 1, 0] for 1 minute
      * **end**: \[hours, minutes, days] (for example: \[0, 1, 0] for 1 minute
    * **platform**: Shows your activity being run on a specific platform
      * **desktop**: The default platform we have used until now
      * **xbox**: Xbox activity
      * **samsung**: Samsung Galaxy activity
      * **ps4**: PlayStation 4
      * **ps5**: PlayStation 5
    * Custom Dynamic Values:
      * Dynamic bot variable
        * Example: `{bot.user.name}` to return your username as a dynamic value
      * Ability to register your own, self-made dynamic {values} to the bot using scripts
        * `addDRPCValue(value, func)` -> More info & examples will be shared regarding custom dynamic values and how to register them properly
* Dynamic values support on themes (embed & text | for webhooks will come later)
* Scripting:
  * ~~await nightySend(ctx, title, description)~~

    -> `await ctx.nighty_send(content: str, title: Optional[str], file: Optional[discord.File])`

    * Example usage: `await ctx.nighty_send(content="Hello world")`
  * &#x20;`await ctx.nighty_help(commands: list[Command], title: Optional[str])`
    * Example usage to show all commands: `await ctx.nighty_help(title="All commands", commands=bot.commands)`
  * `await ctx.nighty_command_help(command: Command)`&#x20;
    * Get help with a command&#x20;
  * user.avatar will return default\_avatar instead of None when the user has no avatar
  * UI Scripts: toggle checkboxes can be updated to be checked on load, and selection values can be updated in real time. The UI scripting API documentation will be updated accordingly
  * Create & add your self-made dynamic values using scripts


# The application did not respond / Unknown Integration

Use this guide to resolve slash command issues related to "This or unknown integration errors.

Press **`Ctrl + R`** to refresh Discord and resolve command response issues. That's it.


# Common solutions to all issues

Facing issues with Nighty? Try these general solutions first, applicable to various problems.

{% hint style="info" %}

## Important Information for Using Nighty on a VPS

To successfully run Nighty on your VPS, please ensure that you install Microsoft WebView2 from our guide [here](/troubleshooting/app-is-just-a-white-box-ui-not-appearing-looking-weird).
{% endhint %}

1. **Disable your antivirus**\
   Disable Windows Defender Real-time protection), including any third-party programs like Malwarebytes, to prevent any potential false positive detections during the installation process. This step helps ensure that Nighty's files are not mistakenly flagged as malicious.
2. **Delete the main folder**
   * Press `Win + R` on your keyboard.
   * Type `%appdata%/Nighty Selfbot` and press Enter.
   * Delete everything inside that folder.
   * Restart Nighty.
3. **Disable Firewall**\
   If you are encountering difficulties launching the bot, try temporarily disabling your firewall. Firewalls may sometimes block connections and prevent proper execution. Ensure that Nighty has the necessary permissions to access the network.
4. **Update Windows**\
   Ensure your operating system is up-to-date by installing the latest Windows updates. Outdated system components may contribute to compatibility issues. Visit the Windows Update settings and download any available updates to enhance system stability.
5. **Verify Nighty.exe Placement.**\
   Double-check that you have correctly extracted 'Nighty.exe' from 'Nighty.zip' and placed it in the 'C:/Nighty' directory. A misplaced or missing executable file can prevent the bot from launching.
6. **Update Nighty**\
   Ensure you have the latest version of Nighty. Visit [nighty.one/get-links](https://nighty.one/get-links) to redownload the launcher. Follow the [First Launch](/getting-started/first-launch) tutorial on the documentation page for a step-by-step guide to update Nighty to its most recent version. Keeping the bot updated can resolve compatibility issues and improve overall performance.
7. **Resource management**\
   Close any unnecessary programs or processes to free up system resources and improve performance.
8. **Restart your computer**\
   To clear any temporary files or processes that may be interfering with the application launch. Often this solution works.
9. **Check Key Status**\
   Verify if your key is valid. Follow the guide [here](/troubleshooting/flagged-key).


# Unable to launch

This section is designed for users who are unable to run the program, experiencing instant crashes or errors during launch.

## Error: Unhandled exception in script

Failed to execute script 'Nighty' due to unhandled exception: (...)

If you encounter this error, please follow the [common solutions guide](/troubleshooting/common-solutions-to-all-issues).

<br>


# App is just a white box, UI not appearing / looking weird

Nighty relies on WebView2 technology to display its Graphical User Interface (GUI). If you encounter an issue where the app is just a white box, and/or the UI is not appearing/looking weird, follow these steps to troubleshoot and resolve the problem.

### Installing WebView2

Nighty requires *Microsoft WebView2*. If it is not installed on your computer or Virtual Private Server, follow these steps:

1. Download the [Evergreen Standalone Installer X64 (click)](https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/dad8096c-1b0c-40c5-9b1c-415164028ec9/MicrosoftEdgeWebView2RuntimeInstallerX64.exe).
2. Install the downloaded installer.
3. Reboot your PC or server.
4. Try running Nighty again.

If the issue persists, it may be due to an incorrect version of WebView2 installed. Follow the steps below to fix this:

#### Verifying and Installing the Correct WebView Version

1. Open PowerShell as an administrator.
   1. Press the Windows key.
   2. Search for "Windows PowerShell."
   3. Right-click on "Windows PowerShell" and select "Open as administrator."
2. Paste the following command into the PowerShell terminal and press Enter.\
   `cd C:\Program Files (x86)\Microsoft\EdgeWebView\Application\1*\Installer`\
   \
   If you got an error, try executing this command instead:\
   `cd C:\Program Files\Microsoft\EdgeWebView\Application\1*\Installer`
3. After that, paste the following command and press Enter:\
   `.\setup.exe --uninstall --msedgewebview --system-level --verbose-logging --force-uninstall`
4. Reboot your PC.
5. Install the correct version of WebView2 by following the steps mentioned in the [Installing WebView 2 section](#installing-webview-2) above.


# Retrieving License Key

Forgot your license key? No worries, follow these steps to retrieve it.

### **Retrieving License Key from Your PC:**

1. Press Win+R
2. Type `%appdata%/Nighty Selfbot`, and press OK.
3. If successful, open *auth.json* with *Notepad* (Right click - open with - Notepad).
4. Locate your license key in auth.json and copy it.

### Retrieving License Key from Order Email:

1. Search your email inbox for the keywords "Nighty," "Shoppy," or "Sellix"
2. Locate your license key in order confirmation email.

### **Retrieving License Key via Nighty Support:**

1. Head to the [Support Portal](https://nighty.support/).
2. Initiate a new support request.
3. Include as much information as possible such as Reseller or Seller name, Date of purchase, Payment method, Email address, Discord ID or name+tag, and email confirmation screenshot.

Nighty Support Team will assist you in retrieving your license key promptly.


# Flagged key

If you suspect your key was flagged, follow these steps.

Flagging is part of our continued effort to maintain the integrity and safety of our users. To unflag your key and restore full access to Nighty, you must submit a verification request through our Support Portal.

### **Checking if key is flagged**

1. Visit [Get Links](https://nighty.one/get-links).
2. Input your license key, click "Show me the links."
3. If links appear, your key is fine. If flagged, follow the guide below.

### **If Key is Flagged**

1. Go to the [Support Portal](https://nighty.support/).
2. Open a new support request, select "Flagged Key category."
3. Provide proof of purchase: confirmation email screenshot, transaction screenshot, and extra purchase details (Reseller, Date, Payment method, Email, Discord ID).


# Rich Presence doesn't show up

If your Rich Presence isn't appearing in Discord, follow these steps to troubleshoot:

1. **Check Your Discord Status**\
   Make sure your Discord status is set to **Online / Do Not Disturb / Idle**. Rich Presence won’t show if you're **Invisible** or **Offline**.
2. **Verify Custom Image URLs**\
   If you're using custom images (large/small), upload them to [Imgur](https://imgur.com/).\
   After uploading, **right-click the image → Open in new tab → Copy the URL** from the address bar. It starts with i.imgur and ends with image extension (eg. .jpg/.png).
3. **Enable Activity Settings in Discord**\
   Go to **User Settings → Activity Privacy** and **enable all options**.
4. **Enable Server-Specific Activity Privacy**\
   In each server where you want Rich Presence visible:\
   **Click the server name (above the channel list) → Privacy Settings → Enable both options.**

#### Still Not Working?

Open a support ticket at [nighty.support](https://nighty.support/) and attach the following files:

* `profile.json`
* `presets.json`

You can find them here:\
Press **Win + R →** type `%appdata%/Nighty Selfbot/data` → **OK**


# Discord MFA Setup

Some things you do on Discord need an extra security check before they go through. Depending on how your account is set up, that check is either your **Discord password** or a **2FA code** (the 6-digit code from an authenticator app like Google Authenticator).

Nighty doesn't know your password or your 2FA code, so it can't pass that check on its own. To use the features that need it, you have to give this information to Nighty once. This guide shows you how.

{% hint style="danger" %}

## Before you start (please read)

### If you already use 2FA (an authenticator app) on your Discord account:

To set 2FA up with Nighty, you first have to **turn 2FA off on Discord and turn it back on again**. This is the only way to get the special key Nighty needs.

Turning 2FA off and on again will **change your Discord token**, which means Nighty will log you out and ask you to log back in. That's normal and expected.

* Set aside about **5 minutes** for this.
* Follow the steps exactly as written, in order.
  {% endhint %}

### Which section do I follow?

**You only use a password** (no authenticator app / no 2FA) → follow the **Password Only** section below.

{% content-ref url="/pages/b297d58ca5c19557acaf4ac0df597bd8f7ea625e" %}
[Password Only](/guides-and-tutorials/discord-mfa-setup/password-only)
{% endcontent-ref %}

**You already use 2FA** (an authenticator app) → go to the **2FA Setup** section below.

{% content-ref url="/pages/a412f27ec637eb4bc2bbb99ee2b2526b3f9cc620" %}
[Password + 2FA (Authenticator app)](/guides-and-tutorials/discord-mfa-setup/password-+-2fa-authenticator-app)
{% endcontent-ref %}

Not sure? Start with [**Password Setup**](/guides-and-tutorials/discord-mfa-setup/password-only). If Nighty asks you for a 2FA code at any point, that means you have 2FA enabled, so jump to the **2FA Setup** section instead.


# Password Only

Use this if you sign in to Discord with just a password (no authenticator app).

{% stepper %}
{% step %}

## Open the Modules tab

In Nighty, open the **Modules** tab.

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FgM7j9Vh6Y32ylyKrGczk%2Fimage.png?alt=media&amp;token=d5f3c7b4-46ea-4c84-b302-3b48edf2c7ba" alt=""><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}

## Open Multi Factor Authentication

Click **Multi Factor Authentication (MFA)**.

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FARd8S68GkJU1l4js6Vm5%2Fimage.png?alt=media&amp;token=25da54a3-8e14-4972-8775-96d789aef1b9" alt=""><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}

## Start configuration

Click **Start Configuration**.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FKyIyfHcMoFKkOJ2qkQlQ%2Fimage.png?alt=media&amp;token=90f80ea5-215e-4baa-8a13-def7a3e9f3c4" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

## Enter your Discord password

Type in your **Discord password**.

{% hint style="warning" %}
Type it carefully. Nighty does **not** check whether it's correct, so a typo here will cause problems later.
{% endhint %}

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FGL7RPsQ6FzLJSQEdmL9E%2Fimage.png?alt=media&amp;token=91db6283-bcaa-4b8e-a3c8-c45379cfdc33" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

## Finish

Click **Finish**.&#x20;

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FPXB8B1xXIgYP8Jf1dN8k%2Fimage.png?alt=media&amp;token=cb757a19-976d-44b1-a930-f730ca968fdc" alt=""><figcaption></figcaption></figure></div>

You should see **Status - Configured**. You're done!
{% endstep %}
{% endstepper %}

{% hint style="info" %}
**Do you see a "Next" button instead of "Finish"?**

If Nighty asks you to enter a 2FA code, that means you have 2FA enabled on your account. Stop here and follow the [**2FA Setup**](/guides-and-tutorials/discord-mfa-setup/password-+-2fa-authenticator-app) section below instead.
{% endhint %}


# Password + 2FA (Authenticator app)

Use this if you already have 2FA (an authenticator app) turned on for your Discord account.

As mentioned earlier, you first need to remove 2FA from Discord and add it back, then log in to Nighty again. Follow every step in order.

{% stepper %}
{% step %}

### **Close Nighty.**

Close Nighty.&#x20;
{% endstep %}

{% step %}

### **Open your Discord MFA settings.**

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FzaRwIxrImz6XnsjBpzfA%2Fimage.png?alt=media&amp;token=01c15830-dc22-43d9-aa88-8c0aef77825d" alt=""><figcaption></figcaption></figure></div>

Go to **Account** → **Password & Security**.

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F1pEGg8QwPgd0uWWUpsyK%2Fimage.png?alt=media&amp;token=8d7afd1c-2d3c-49b1-a1d8-4277b1e3daf4" alt=""><figcaption></figcaption></figure></div>

Click **Multi-Factor Authentication**.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FrvY6vQTdf8d04vNBpDwg%2Fimage.png?alt=media&amp;token=f0987396-7344-47f9-a200-e495fc937241" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### **In the Authenticator App section, click Remove and confirm.**

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FhJ6R30p9qQjgEBHOn65S%2Fimage.png?alt=media&amp;token=bec8b719-32f8-46a2-9bc3-8f9f4db615a1" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FyrwqAsRJbbmVCR0ygWXR%2Fimage.png?alt=media&amp;token=28170fe7-73f6-4643-b559-c676c1199374" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### **Open your authenticator app (for example, Google Authenticator) and delete your Discord account from it.**

{% hint style="danger" %}

## Double check if 2FA was removed from your Discord account.

You may be locked out of your Discord account if you remove 2FA from your authenticator app before disabling it on Discord.

Before removing it from your authenticator app, verify in Discord that authentication is password-only and MFA is disabled.
{% endhint %}

Make sure 2FA is removed from your Discord account and remove it from your Authenticator app as well - to avoid mistakes in the future.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FFAwGDjigZCoQNx90xAhI%2Fremove_discord_mfa_code-optimized.gif?alt=media&amp;token=04b71b45-5fab-4593-8d1b-9d9a81e0aa93" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Add 2FA back and save your key

Back in Discord, click **Enable Authenticator App**.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FE5ry9ideFsUEXU8m8Whe%2Fimage.png?alt=media&amp;token=beb156c3-440c-4be9-ac18-fa009ddafb68" alt=""><figcaption></figcaption></figure>

Find the option for **manual entry** (the text key) and **copy that key into Notepad**. Keep Notepad open, you'll need this key soon.

### ⚠️ Save the manual entry code! You will need it soon.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FugH72MRpP0TevDHdK5rL%2Fimage.png?alt=media&amp;token=d1b17fc0-f1f6-4901-b5e7-06a024167dd2" alt=""><figcaption></figcaption></figure>

Now scan the QR code with your authenticator app to add Discord back to it (standard procedure).

Finish the process on Discord by entering the 6-digit code from your authenticator app when it asks. Click Activate to Activate 2FA on your Discord account.

{% hint style="warning" %}

### Only contine to the next step if you succesfully activated 2FA on your Discord account and saved your 2FA key!

{% endhint %}
{% endstep %}

{% step %}

### Log back in to Nighty

Because you turned 2FA off and on, your Discord token changed. Nighty will notice this and ask you to log in again.

Open **Nighty**.

Log in with your preferred method.

* If you're not sure how, see the [**First Launch**](/getting-started/first-launch) guide.

\ <sup>Note: If you don't want to create a new app simply close the Create Application popup and click on your previous application.</sup>
{% endstep %}

{% step %}

## Open the Modules tab

In Nighty, open the **Modules** tab.

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FgM7j9Vh6Y32ylyKrGczk%2Fimage.png?alt=media&amp;token=d5f3c7b4-46ea-4c84-b302-3b48edf2c7ba" alt=""><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}

### Open Multi Factor Authentication

Click **Multi Factor Authentication (MFA)**.

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FARd8S68GkJU1l4js6Vm5%2Fimage.png?alt=media&amp;token=25da54a3-8e14-4972-8775-96d789aef1b9" alt=""><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}

### Start configuration

Click **Start Configuration**.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FKyIyfHcMoFKkOJ2qkQlQ%2Fimage.png?alt=media&amp;token=90f80ea5-215e-4baa-8a13-def7a3e9f3c4" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Enter your Discord Password

Type in your **Discord password** and click Next.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FwyPE5UezM1vymUqgclIQ%2Fimage.png?alt=media&amp;token=0266ae65-c124-463f-b533-06111ced782d" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
Type it carefully. Nighty does **not** check whether it's correct, so a typo here will cause problems later.
{% endhint %}
{% endstep %}

{% step %}

### Enter your 2FA 28-digit key

Remember the Manual entry 2FA code you saved eariler in [step 5](#save-the-manual-entry-code-you-will-need-it-soon)?

Copy it to clipboard now and paste to Nighty and click Finish.

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FdCZGfMlXwd2quQ9mcUrS%2Fimage.png?alt=media&amp;token=8d0a583e-f6aa-4413-816d-632125f04821" alt="Screenshot of the code from Step 5." width="375"><figcaption><p>Screenshot of the code from Step 5.</p></figcaption></figure></div>

Now paste it into Nighty and click Finish.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F2DJBVshUB1SKQCNsnlaK%2Fimage.png?alt=media&amp;token=674441f5-3231-42c7-826a-cdb35aa0f842" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Confirm with 6-digit code from your Authenticator app

Open your Authenticator app and enter your 6-digit code (the same you normally use to login to Discord)

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FbVtAmN44RApIytUjf2lP%2Fimage.png?alt=media&amp;token=9d1ff85b-190e-4d6c-b22c-a29b3514b3b7" alt=""><figcaption></figcaption></figure></div>

Click **Verify Code**.

You should then see **Status - Configured**. You're done!

<div align="left"><figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FPXB8B1xXIgYP8Jf1dN8k%2Fimage.png?alt=media&amp;token=cb757a19-976d-44b1-a930-f730ca968fdc" alt=""><figcaption></figcaption></figure></div>
{% endstep %}
{% endstepper %}

## After configuring:

* **You can close Notepad** and delete the key from it. You should not share it with anyone.


# Quickstart Guide

We strongly recommend reading the full documentation, but if you’re short on time, here’s a quick-start guide to get you up and running.

A custom feature is one sentence: **when this happens, check a few things, then do something.**

## The five parts

Every feature is built in this order, and it runs in this order too:

<table><thead><tr><th width="89">N</th><th width="161">Part</th><th>What it's for</th></tr></thead><tbody><tr><td>1</td><td><a href="/custom-features-tab/understanding-events-and-parameters"><strong>Event</strong></a></td><td>What sets it off - a message, someone joining, a timer</td></tr><tr><td>2</td><td><a href="/custom-features-tab/execution-limits"><strong>Limits</strong></a></td><td>How often it's allowed to run</td></tr><tr><td>3</td><td><a href="/custom-features-tab/filters"><strong>Filters</strong></a></td><td>Broad "where and who" checks. Fast way to rule things out</td></tr><tr><td>4</td><td><a href="/custom-features-tab/conditions"><strong>Conditions</strong></a></td><td>The detailed checks. Only what you want gets past here</td></tr><tr><td>5</td><td><a href="/custom-features-tab/actions"><strong>Actions</strong></a></td><td>What actually happens</td></tr></tbody></table>

If any step says no, the feature stops there and nothing happens.

## Six things to know before you start

**1. Every** [**event**](/custom-features-tab/understanding-events-and-parameters) **brings information with it.** A new message gives you the message, who sent it (user or member), the channel, the server (guild). You build everything out of these [**parameters**](/custom-features-tab/understanding-events-and-parameters).&#x20;

**2. "Guild" means "server."** Same thing.

**3. Not everything is always there.** DMs have no server and no member. Features that work in servers can silently do nothing in DMs.

**4. Leave** [**safe mode**](/custom-features-tab/safe-mode) **ON.** New features have it on by default. Your feature runs for real but stops before doing anything, and tells you what it *would* have done. Turn it off only once you've watched it behave correctly.

**5. Add the "Ignore self" or "Only Self" filter.** Always. Without it, a feature that replies to messages will reply to its own reply, forever.

**6. Keep the default limit of 1 run per second.** It's your brake. Only remove it if you know exactly why.

## Your first feature, in 2 minutes

{% stepper %}
{% step %}

## Pick the event <mark style="color:$success;">**New Message**</mark>

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F4qZUxSzegWwxd5Vyadsp%2Fimage.png?alt=media&amp;token=91a4c0fa-ab32-4348-b8c1-41b073177c79" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

## Add the filter <mark style="color:$success;">**Ignore self**</mark>

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FxE4iY2JsvFz1wYKchXed%2Fimage.png?alt=media&amp;token=e7f226b1-d26f-411e-b78d-8545c815145c" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

## Add a condition

<mark style="color:$success;">Message contains</mark> `hello`.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FKWC6ahXbn8xCXi7sLcfk%2Fimage.png?alt=media&amp;token=3edaa04d-12b5-4a98-9f21-e95636c48b11" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

## Add a condition

<mark style="color:$success;">Message mentions me</mark>

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FluqakDYZX3JDinJ1FV5c%2Fimage.png?alt=media&amp;token=0edf225f-e528-446d-88df-cebb7de0ea2b" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Keep "if met, just continue" and "otherwise, stop" as is

Do not change anything there, keep them collapsed.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FQ3mXbNZlVVKDK4SF1TbU%2Fimage.png?alt=media&amp;token=f7cddc98-4dc7-4841-9eef-3c166986b9b9" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

## Add the <mark style="color:$success;">action</mark>

**Reply** → `Hi there!`

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F7My3MS9J7Sks45kq0YTs%2Fimage.png?alt=media&amp;token=f682add2-41b3-44f3-91c5-effe67ace785" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

## Test in safe mode

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F4nfY80iD4oCHUbLLkWZW%2Fimage.png?alt=media&amp;token=ca051751-153a-47b3-b28f-19372d5ef513" alt=""><figcaption></figcaption></figure>

Go to the Overview Tab.

Ask your friend to say "hello" and ping you (for example `hello @you` on Discord.

If a notification appears saying actions were about to run - it works.&#x20;

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FCCVCB8aGiB5pGr6VA3ec%2Fimage.png?alt=media&amp;token=1cdc3b2c-717d-4679-a15b-98d930f15ef9" alt=""><figcaption></figcaption></figure>

Turn safe mode off and try it for real, again.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FDYmY5wbE2B6TcfPEiaFZ%2Fimage.png?alt=media&amp;token=764cebed-14d5-42b1-ac77-29107c468538" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FMVCfPWSMrHn5HEozWJGS%2Fimage.png?alt=media&amp;token=96217986-87cf-4820-ae32-ab99caac2721" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

## When something goes wrong

**Check the feature's log first.** Nearly every problem writes its reason there: which value was empty, which ID couldn't be found, which action failed.

The three most common causes:

| It's…                 | Look at                                                                                  |
| --------------------- | ---------------------------------------------------------------------------------------- |
| Doing nothing         | A filter or condition is blocking it - or it's a DM and you used a server-only parameter |
| Doing too much        | Conditions too loose, or a missing **Ignore self**                                       |
| Doing the wrong thing | A reference is pointed at the wrong parameter                                            |

## A rules that will save you

{% hint style="danger" %}
**Test in safe mode. Every time.**

**If safe mode is spamming notifications, your feature is broken.** Fix it before turning safe mode off - that spam would be going to Discord.
{% endhint %}


# Understanding Events and Parameters

## Discord is constantly sending you events

Let's start with something you do every day.

You open a server, click on **#general**, and you see people chatting. Messages appear on your screen the moment someone sends them.

Have you ever wondered *how* they get there?

Here's what actually happens. Every time someone sends a message, Discord sends a small notification to your Discord app that says, roughly:

> "Hey - something just happened. A new message was created. Here are the details."

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FhuLbNkwh9pd4wmi9ccdS%2FMessage_DC.gif?alt=media&amp;token=2d8976bf-b8fa-4269-bf8e-89aaa9017971" alt=""><figcaption></figcaption></figure>

That notification is called an **event**. Your Discord app receives it, reads the details, and draws the message on your screen.

**An event is simply:&#x20;*****something happened*****, plus&#x20;*****the details about it*****.**

You never see events yourself - they happen silently in the background, hundreds of times a day. But they are always there.

### Events are not just messages

New messages are the most obvious example, but Discord sends an event for almost everything that happens around your account:

* A message is sent, edited, or deleted
* Someone reacts to a message with an emoji
* A member joins or leaves a server
* A role is given to or taken from a member
* Someone changes their nickname
* A channel is created, renamed, or deleted
* Someone joins or leaves a group DM
* A user changes their username or avatar

Every single one of those is an event.

### Where Nighty comes in

Nighty listens to the exact same events your Discord app does.

**Custom Features let you say: "When THIS event happens, do THAT."**

That's the core idea. Everything else in this documentation is just detail on top of it.

{% hint style="info" %}
**Important:** a Custom Feature does nothing until its event happens. If you build a feature that reacts to new messages, it sits quietly doing nothing until somebody sends a message. This is normal - it's not broken.
{% endhint %}

## Every event carries parameters

An event isn't just a signal saying "something happened." It also carries **information about what happened**.

Think of an event as a **delivered package**. The package tells you *something arrived*. But what's useful is **what's inside the box**.

The things inside the box are called **parameters**.

When someone sends a message, the **New Message** event arrives carrying these parameters:

| Parameter   | What it is                                                        |
| ----------- | ----------------------------------------------------------------- |
| **message** | The message itself                                                |
| **user**    | The person who sent it - you, a friend, a stranger, or a bot      |
| **channel** | Where it was sent - a text channel like #general, or a DM         |
| **guild**   | The server it was sent in *(only if it wasn't a DM)*              |
| **member**  | The sender, as a member of that server *(only if it wasn't a DM)* |
| **me**      | You - your own Discord account. Always available                  |

Those are the pieces of information Nighty hands you to work with.

{% hint style="info" %}
We say **Guild** where Discord's interface says **Server**. They mean exactly the same thing. Discord's own developers call servers "guilds," and Nighty follows that. Whenever you read **guild** in Nighty, think **server**.
{% endhint %}

## A parameter is not one value - it's a container of details

This is the part that clicks for most people once they see it.

A parameter isn't a single piece of text. It's a **container** holding many details inside it.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FkavB0ZxCn3umml7k1svW%2Fdvalues1exp.jpg?alt=media&amp;token=f727a2b4-4229-445b-a01b-f33744afe4e7" alt=""><figcaption></figcaption></figure>

Look at the message above on your screen right now. Discord is showing you several separate details at once:

* The **avatar** on the left (User → Avatar)
* The **name** in bold (User → Name)
* The **content** of the message (Message → Content)

Discord got all of those out of the parameters that came with the event - and you can get them out too.

You'll see these details written in blue boxes in the Custom Features Tab, like this:

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F5uQ0twWgwJkpEwIaRuPw%2Fimage.png?alt=media&amp;token=8431310e-0c91-4026-b17c-a49c80ab63f8" alt=""><figcaption></figcaption></figure>

You never have to type them by hand - the editor gives you an Expression Builder and inserts them for you. Read it as **"the `content` detail, taken from the `message` parameter."**

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FPFWYJiFRRNGlRwwDyXIz%2Fimage.png?alt=media&amp;token=cf520cb0-ae08-4705-b953-130e4695b164" alt=""><figcaption></figcaption></figure>

{% hint style="success" %}
**The mental model:** the **event** tells you *what happened*. The **parameters** tell you *who and where and what was involved*. The **details** inside each parameter are the actual values you use.
{% endhint %}

## Parameters that sound the same but aren't

Some parameters have similar names and it's easy to mix them up. This section is worth reading twice - most beginner confusion lives right here.

### `user` vs `member` - the important one

Both refer to **the same person**. The difference is *which side of them* you're looking at.

**`user` is the general Discord account.** Every human (and bot) on Discord is a user. This is who they are everywhere, across all of Discord. Their username, their avatar, their account age. **A `user` is always available**, no matter where the event happened.

**`member` is that same person&#x20;*****inside one specific server*****.** A member is a user who has joined a server, and being a member comes with server-specific things: their nickname on that server, the roles they have there, when they joined, and what can be done to them there - kick, ban, timeout, add role, remove role.

{% hint style="warning" %}
**`member` only exists when the event happened inside a server.** If someone sends you a direct message, there is no server involved - so there is **no member**, only a **user**.
{% endhint %}

An example. Imagine your friend Anna:

|                    | As a `user`               | As a `member`                         |
| ------------------ | ------------------------- | ------------------------------------- |
| Who                | Anna, the Discord account | Anna, in *this particular server*     |
| Username           | `annabanana`              | *(same — it comes from the user)*     |
| Nickname           | ❌ doesn't exist           | ✅ "Anna 🌸" - but only on this server |
| Roles              | ❌ doesn't exist           | ✅ Moderator, Verified                 |
| Can you kick them? | ❌ no such thing           | ✅ yes, from this server               |
| Available in a DM? | ✅ always                  | ❌ never                               |

Anna is one person. In a server she's both a user *and* a member. In your DMs she's only a user.

**How to choose:**

* Want their username, avatar, ID, or an @mention? → use **`user`**
* Want their nickname, roles, join date, or to kick/ban/timeout them? → use **`member`**

### `channel` vs `server_channel` vs `group_channel`

All three are places where messages live. They differ in *what kind* of place.

| Parameter           | What it means                        | Works in                        |
| ------------------- | ------------------------------------ | ------------------------------- |
| **channel**         | Any channel at all - the general one | Server channels, DMs, group DMs |
| **server\_channel** | A channel that belongs to a server   | Servers only                    |
| **group\_channel**  | A group DM with several people       | Group DMs only                  |

`channel` is the safe general choice. Use `server_channel` when you need something that only server channels have - a topic, a category, server-specific settings.

### `message` vs `message_before_edit` vs `message_replied_to`

These show up on message events and each points at a *different* message:

| Parameter                 | Which message it is                                            |
| ------------------------- | -------------------------------------------------------------- |
| **message**               | The message the event is about                                 |
| **message\_before\_edit** | What that message looked like **before** it was edited         |
| **message\_replied\_to**  | The **older message** this one is replying to, if it's a reply |

So on a **Message Edit** event, `message` is the new version and `message_before_edit` is the old version. Comparing the two is how you build something like an edit-logger.

### "before" and "after" parameters in general

Whenever something *changes*, Nighty gives you both versions, so you can compare them:

* **Nickname changed** → `old_nick` and `new_nick`
* **Channel renamed** → `old_name` and `new_name`
* **Channel topic changed** → `old_topic` and `new_topic`
* **Member changed somehow** → `member_before_update` and `member`
* **User changed somehow** → `user_before_update` and `user`

### `me` - always there

Every single event carries a `me` parameter: **your own Discord account.**

It doesn't matter what happened or where - `me` is always available. Use it when you need your own name, avatar, status, friend count, or server count.

## Not every parameter is available every time

This is the number one reason a feature "doesn't work" for beginners.

**The parameters you get depend on where the event happened.**

Compare the same New Message event in two places:

**A message in a server's #general:**

✅ message ✅ user ✅ channel ✅ guild ✅ member ✅ me

**The exact same message, sent as a DM:**

✅ message ✅ user ✅ channel ❌ guild ❌ member ✅ me

There's no server, so there's no `guild` and no `member`. They simply aren't in the box.

Some parameters are conditional in other ways too. `message_replied_to` only exists if the message is actually a reply to something.

{% hint style="info" %}
**Rule of thumb:** if a feature works in servers but seems to do nothing in DMs, you're almost certainly using `guild` or `member` somewhere. Switch to `user` and `channel`, which always exist.
{% endhint %}

### Special events

These two are different - they don't come from Discord at all.

| Event              | Fires when                                                                                              |
| ------------------ | ------------------------------------------------------------------------------------------------------- |
| **Repeat Action**  | On a timer you choose. "Every 60 seconds", "every 10 minutes." Nothing has to happen on Discord at all. |
| **Custom Feature** | When *another* Custom Feature triggers it on purpose. You decide which parameters get handed over.      |

**Repeat Action** is for anything you want to happen on a schedule - post a message every hour, check something regularly, send yourself a reminder.

**Custom Feature** lets you split a big automation into smaller reusable pieces, and have one call another. You'll get a full page on this later.

*(And remember: every event above also carries **me**.)*


# Custom Parameters

On [Understanding Events and Parameters Page](/custom-features-tab/understanding-events-and-parameters) you learned that every event hands you a set of parameters automatically. But sometimes the thing you need simply isn't one of them.

**Custom parameters let you add your own.**

## When you need one

Here's the clearest example.

You want to post an automated message in a specific channel every hour. So you pick the **Repeat Action** event… and you have nothing to work with. Repeat Action isn't triggered by anything on Discord - nobody sent a message, nobody joined a server - so there's no `channel`, no `user`, no `guild`. The event carries no parameters at all.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FTenscuZs1EB6CjbKAqR7%2Fimage.png?alt=media&amp;token=8c39f4fc-6704-43e4-a560-96d0b4cba822" alt=""><figcaption></figcaption></figure>

But you *need* a channel. Specifically, the one channel you want to post in.

That's what a custom parameter is for: **you tell Nighty which channel you mean, and it goes and gets it for you.**

The same applies any time you need something the event didn't hand you:

* Post in a **different** channel than the one the event happened in
* Check a role in **another** server
* Read a specific pinned message that has nothing to do with the current event
* Look up a specific person who isn't involved in what just happened

## Adding one

You add custom parameters in the **parameters** section of your feature. The popup asks you for a few things:

{% stepper %}
{% step %}

## The type

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Fz2vRNgGrS0JiS1BC3AgI%2Fimage.png?alt=media&amp;token=12d86023-a6aa-41f6-b26e-d95f09999b80" alt=""><figcaption></figcaption></figure>

What kind of Discord thing is it? Your choice here decides which IDs Nighty needs from you.
{% endstep %}

{% step %}

## A name

This is how you'll refer to it everywhere else in the feature, so make it obvious to your future self. `general_channel` is good. `chan2` is not.
{% endstep %}

{% step %}

## A description (Refers To)

What this parameter is for. You'll thank yourself in three months.

> **Example**
>
> Name: `general_channel`\
> Refers to: *the channel I want to send automated messages in*
> {% endstep %}

{% step %}

## Availability

*When* Nighty should go and fetch it. This needs its own explanation - see [Availability — when Nighty goes and gets it](#availability--when-nighty-goes-and-gets-it).
{% endstep %}
{% endstepper %}

## How a parameter is actually obtained

To understand availability, you first need to know what "obtaining" means.

**Discord doesn't tell your app everything up front.**

When you launch Discord - or Nighty - it does **not** download every member of every server, every channel, and every message. That would be enormous. Instead it starts with very little and **learns as it goes**:

* Someone sends a message → now it knows about that person and that channel
* You open a channel → now it knows that channel and the recent messages in it
* You scroll the member list → now it knows those members

Everything it has learned is kept in a **cache** -a short-term memory of things it has seen recently.

So when your feature says *"give me the channel with this ID"*, one of two things happens:

| Situation                  | What happens                                                                                                        |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| ✅ **Already in the cache** | Instant. Nighty already knows it. Costs nothing.                                                                    |
| ⏳ **Not in the cache**     | Nighty has to **ask Discord**: *"tell me about the channel with this ID."* That's a real request over the internet. |

That second case is the important one. Asking Discord:

* **takes time** - it's a round trip to Discord's servers
* **counts against your rate limits** - Discord only lets you ask so many questions so quickly

None of this is a problem when it happens occasionally. It becomes a problem when it happens on **every single message in every server you're in**.

Which is exactly what availability controls.

{% hint style="warning" %}
**Wrong IDs get given up on.** If Nighty fails to fetch the same ID **3 times**, it stops trying that ID entirely and your parameter will just be empty from then on. This protects you from a typo generating endless failed requests. If you fix a wrong ID, **restart Nighty** so it forgets the failures and tries again.
{% endhint %}

## Availability - when Nighty goes and gets it

Availability is the point in your feature where Nighty asks Discord for the parameter. There are three choices:

| Setting                                | Fetched                                                        | Usable in                                 |
| -------------------------------------- | -------------------------------------------------------------- | ----------------------------------------- |
| **Everywhere**                         | Fetched as soon as the event fires, on **every** event, always | Everything (Filters, Conditions, Actions) |
| **Conditions and actions**             | Only if your filters passed                                    | Conditions and actions                    |
| **Actions** (final actions block only) | Only if your filters **and** conditions passed                 | Actions only                              |

Think of it as three gates. The later you set it, the more events have already been thrown away before Nighty bothers to fetch anything.

### Why "as late as possible" is the rule

Picture a feature on **New Message** in a busy server. A thousand messages an hour come through. Your filters and conditions narrow that down to the five you actually care about.

| Availability | Requests to Discord per hour                                  |
| ------------ | ------------------------------------------------------------- |
| Earliest     | **1000** - one for every message, even the 995 you threw away |
| Latest       | **5** - only the ones that got all the way through            |

Same feature, same result, 200× less work.

{% hint style="success" %}
**The rule: pick the latest option that still works.**

If you only use the parameter in your actions, choose **actions only**. If a condition needs to check it, choose **after filters**. Only choose the earliest option if you genuinely have no other choice.
{% endhint %}

{% hint style="info" %}
In reality there are some safety checks behind the scenes not allowing Nighty to make that many requests to Discord, but you should still use the "as late as possible" principle.
{% endhint %}

### When you *do* need it earlier

Move it earlier only when something earlier in the chain needs it:

* **A condition uses it** → it must be fetched **after filters** at the latest
* **Nothing else uses it** → **actions only**

There's one thing custom parameters can **never** be used for, no matter what you pick: **filters**. Filters run before any custom parameter has been fetched, so they can only use the event's own built-in parameters.

## Worked examples

### Automated hourly message in one channel

* Event: Repeat Action
* Custom parameter: type **Channel**, name `announcement_channel`, the channel's ID
* Availability: **actions only** - nothing else needs it
* Action: send a message to `announcement_channel`

### Log deleted messages to a private channel

* Event: Message Delete
* Custom parameter: type **Channel**, name `log_channel`, your private channel's ID
* Availability: **actions only**
* Action: send the deleted message's details to `log_channel`

Notice the pattern: whenever the parameter is only used at the very end, it's set to **actions only**. That's the default you should reach for.

## A note on the Custom Feature event

There's one case where a custom parameter arrives **without** being fetched at all: the **Custom Feature** event, where another feature hands parameters over to this one directly.

In that case the thing has already been loaded by the feature that sent it, so there's no ID to provide and no request to Discord.


# Execution Limits

## Why limits exist

Remember from [Events and Parameters Page](/custom-features-tab/understanding-events-and-parameters): events fire constantly. A busy server can produce hundreds of messages a minute, and **every single one** of them wakes up your feature.

Without a limit, a feature that replies to messages would reply to *all of them*, as fast as they arrive. That's how people get rate-limited by Discord or spam a channel by accident.

**Execution limits are your safety brake.** They cap how often a feature is allowed to actually do something.

Nighty gives you two kinds:

| Kind                 | Question it answers                                                                     |
| -------------------- | --------------------------------------------------------------------------------------- |
| **Global limit**     | How often may this feature run *in total*?                                              |
| **Parameter limits** | How often may it run *for each individual parameter (person / server / channel / ...)*? |

You can use one, the other, or both together.

## The global limit

The global limit is the simple one:

> **This feature may run X times per Y seconds.**

It counts **every** run of the feature, no matter who triggered it or where.

**The default is 1 time per 1 second**, and you should usually leave it there or make it stricter. It costs you almost nothing in normal use, and it's the thing standing between you and a runaway feature that fires two hundred times in a minute.

{% hint style="warning" %}
**We strongly recommend keeping the global limit at 1 per 1 second or higher.** Only remove it if you have a specific reason and you're sure the feature is safe to run without a brake.
{% endhint %}

A few examples of what different settings mean:

| Setting                   | Meaning                                                         |
| ------------------------- | --------------------------------------------------------------- |
| 1 time per 1 second       | The default. At most one run each second.                       |
| 1 time per 60 seconds     | At most one run per minute, no matter how busy things get.      |
| 5 times per 60 seconds    | Up to 5 runs a minute. A 6th trigger in that minute is ignored. |
| 10 times per 3600 seconds | Up to 10 runs per hour.                                         |

**How the window works:** it's a rolling window, not a fixed clock. "5 times per 60 seconds" means *"in the last 60 seconds, have I already run 5 times?"* - it does not reset on the minute. As old runs age past 60 seconds they drop off and free up space again.

## Limits based on parameters

This is the powerful one, and it's easier than it sounds.

A global limit treats everyone the same. If your feature just ran, it's on cooldown **for everybody** - the next person to trigger it gets ignored, even though it was somebody else who used up the run.

Often that's not what you want. What you usually want is:

> *"Once per person. Anna using it shouldn't stop Ben from using it."*

That's exactly what a parameter limit does.

### The idea

You pick a **parameter from your event** - `user`, `guild`, `channel`, `member`, `role`, and so on - and set a limit on it.

Nighty then keeps **a separate counter for every individual one of those things.**

Set a limit on `user`, and every single person gets their own private counter. Anna has hers, Ben has his. They don't affect each other at all.

> 🎟️ **Think of it like a ticket booth.** A global limit is one ticket for the whole room - once it's taken, nobody else gets in until it's back. A parameter limit hands **everyone their own ticket**. Using yours doesn't touch mine.

### A worked example

Say you build a feature that replies with a welcome message when someone posts. You set:

> **Parameter limit on `user`: 1 time per 60 seconds**

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F4CwfwP8nBGcxf8heKws1%2Fimage.png?alt=media&amp;token=5b1273e7-d22b-4e83-a509-e5ddadaccf3a" alt=""><figcaption></figcaption></figure>

Here's what happens:

| Time     | What happens                   | Result                                   |
| -------- | ------------------------------ | ---------------------------------------- |
| 12:00:00 | **Anna** sends a message       | ✅ Feature runs - Anna's 60 seconds start |
| 12:00:05 | **Anna** sends another message | ❌ Blocked - Anna is on cooldown          |
| 12:00:06 | **Ben** sends a message        | ✅ Feature runs - Ben has his own counter |
| 12:00:20 | **Ben** sends another message  | ❌ Blocked - Ben is on cooldown           |
| 12:01:01 | **Anna** sends a message       | ✅ Runs - Anna's 60 seconds are up        |

One feature, one limit, but **each person is tracked separately**.

### Two things to know

**Your own account is never limited.** The `me` parameter can't be used for limits. There'd be no point - there's only one of you, so a limit on `me` would just be the global limit again.

**If the parameter isn't in the event, the limit is skipped.** Remember from [Events and Parameters Page](/custom-features-tab/understanding-events-and-parameters) that a DM has no `guild` and no `member`. If you put a limit on `guild` and the feature is triggered by a DM, there's no server to count against, so that particular limit simply doesn't apply to that run. Your other limits (including the global one) still do.

{% hint style="info" %}
That's another good reason to always keep the global limit on - it's the one that's guaranteed to apply every time.
{% endhint %}

## "X times per Y seconds" vs "X times total"

Every limit has two settings: **how many times**, and **per how many seconds**.

But you can also switch the time part **off**. That changes the meaning completely:

| Setup                      | Meaning                                                                |
| -------------------------- | ---------------------------------------------------------------------- |
| 3 times **per 60 seconds** | 3 runs allowed in any 60-second window. Space frees up as time passes. |
| 3 times, **time off**      | 3 runs allowed **in total**. Once used, that's it - it never frees up. |

The second one is a **hard budget** rather than a cooldown. It's useful for one-shot things: *"welcome each new member exactly once"* (put it on `member` with the time off), or a feature you want to fire a fixed number of times and then stop.

{% hint style="info" %}
A total budget isn't permanent forever - it resets when the limit is reset.
{% endhint %}

## When does a run actually get counted?

This one surprises people, and it's good news.

**A trigger only counts against your limits if the feature actually did something.**

Walk through what happens when an event arrives:

{% stepper %}
{% step %}

### The event fires

The event fires and the feature wakes up.
{% endstep %}

{% step %}

### Filters and conditions are checked

Filters and conditions are checked.
{% endstep %}

{% step %}

### Nothing passes

**If nothing passes and there are no actions to run** → nothing is counted. Your limit is untouched.
{% endstep %}

{% step %}

### Actions run

**If actions do run** → *that's* when the run is counted and your limits are consumed.
{% endstep %}
{% endstepper %}

So a feature that reacts only to messages containing "hello" does **not** burn its cooldown on the hundreds of other messages it looked at and ignored. It only spends a run when it genuinely replies.

This means you can set a tight limit like "1 per 60 seconds" without worrying that background chatter will silently eat it.

## When limits reset

Your limit counters are cleared in these situations:

| What you do                  | Effect                            |
| ---------------------------- | --------------------------------- |
| **Edit the feature**         | All of its limits reset to empty  |
| **Turn it off and on again** | All of its limits reset           |
| **Rename it**                | All of its limits reset           |
| **Restart Nighty**           | All limits for all features reset |

Counters are kept in memory while Nighty is running, not saved to disk. So if you set a "5 times total" budget and restart Nighty, you get a fresh 5.

{% hint style="info" %}
This is handy while you're building something. If you're testing and keep hitting your own cooldown, just toggle the feature off and back on to clear it.
{% endhint %}


# References

A **reference** is how you tell a condition, filter, or action **which parameter to use**.

Everything you build points at something. A reference is the pointing.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F4bwQhswTp3UNZmmIk9Yg%2Fimage.png?alt=media&amp;token=467d2147-925d-40a4-88f2-21f68f78be31" alt=""><figcaption></figcaption></figure>

## The "which one?" question

Take the **Delete** action. It deletes a message - but *which* message?

That's what the reference answers. When you add the action, it asks you to pick a message, and you choose the one you mean.

Same for conditions. "Message contains 'hello'" - which message? Same for filters. "Ignore bots" - the bot check has to be pointed at a user, because - is the user a bot?&#x20;

{% hint style="info" %}
**Every condition, filter, and action that works on something has a reference picker.** It's usually the first thing it asks you for.
{% endhint %}

## Why it isn't always obvious

If every event carried exactly one message and one user, references would be pointless - there'd only ever be one answer.

But events often carry **several things of the same kind**. From [Understanding Events and Parameters Page](/custom-features-tab/understanding-events-and-parameters), a **Message Edit** event gives you:

* `message` - the message as it is now
* `message_before_edit` - how it looked before
* `message_replied_to` - the older message it's replying to

Three messages. "Message contains 'hello'" means three completely different things depending on which one you point it at:

| Pointed at            | The check becomes                                                         |
| --------------------- | ------------------------------------------------------------------------- |
| `message`             | Does the **new** version contain "hello"?                                 |
| `message_before_edit` | Did the **old** version of the message (before it was edited) contain it? |
| `message_replied_to`  | Does the message being **replied to** contain it?                         |

Same condition. Three different features.

{% hint style="warning" %}
**Read the reference picker before you move on.** The default is usually right, but when it isn't, everything downstream is quietly wrong - and it'll look like the condition is broken rather than mis-aimed.
{% endhint %}

## Some things need more than one reference

**Forward** is a good example. It forwards a message to a channel, so it asks you twice:

Which message should be forwarded?\
Which channel should it go to?

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FIXZpadRUigQs64oYd9aq%2Fimage.png?alt=media&amp;token=a3d2e5ec-11fd-443a-bec1-9fa883db32db" alt=""><figcaption></figcaption></figure>

Two references, two different types. The same shape shows up anywhere an action involves two things - moving something from A to B, or comparing one thing to another.

### Two limits worth knowing

**Filters only ever see the event's own parameters.** Custom parameters haven't been fetched yet when filters run, and no action has happened, so neither can appear there.

## Action references

Most actions just do their job and finish. Some **hand something back**, and that result becomes available to the actions below.

These are called **action references**.

Examples:

| Action                            | Hands back                          |
| --------------------------------- | ----------------------------------- |
| **Send Message** *(in a channel)* | Message - The message it just sent  |
| **Create Invite**                 | Invite - The invite it just created |

When you add one of these actions, it offers to expose its result and lets you name it. Once named, it appears in the reference pickers of every action **below** it - and its details are available as Values in Expressions too, so you can use things like the sent message's ID or link.

### The two rules

{% hint style="info" %}
**1. Only actions&#x20;*****below*****&#x20;can use it.** The result doesn't exist until the action has run, so anything above it can't see it. Order matters.

**2. Actions only.** Filters and conditions have already finished by the time any action runs, so action references never reach them.
{% endhint %}


# Filters

Filters are the **first layer** of narrowing a feature down to exactly what you want.

An event like "New Message" fires for *every* message your account can see - every server, every channel, every person, every bot. That's almost never what you want. Filters are how you say **"only these places, only these people."**

{% hint style="info" %}
![](https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FVg4P5OfTtHxNSBXqwMaj%2Fimage.png?alt=media\&token=7e540730-8251-431a-b8c0-e7ea037955af)

**Think of a filter as a bouncer at the door.** Before your feature does anything at all, the bouncer looks at what just happened and decides whether it even gets to come in.
{% endhint %}

## Filters run first, and they're all-or-nothing

When an event arrives, filters are checked **before anything else** - before your conditions, and long before any action runs.

Two rules to remember:

{% hint style="warning" %}
**1. Every filter must pass.** If you add five filters, all five have to agree. There's no "or" between them - one "no" and the feature stops right there.

**2. If a filter blocks, nothing happens at all.** No actions, and it doesn't count against your [execution limits](/custom-features-tab/execution-limits). The event is simply dropped.
{% endhint %}

That second point is why filters are the *right* place to rule things out. A filter is the cheapest possible "no."

## Filters only work on the event's own parameters

Filters can only be applied to the **built-in parameters that come with your event** - the ones from [Events and Parameters Page](/custom-features-tab/understanding-events-and-parameters), like `user`, `channel`, `guild`, `member`, `message`, `role`.

They **cannot** use parameters you create yourself with **"Add a parameter."**

{% hint style="info" %}
If you need to narrow something down using a parameter you added yourself, that belongs in **conditions**, not filters. Conditions run later, once everything is loaded.
{% endhint %}

## Whitelists and blacklists

This is what most people use filters for. Every major parameter supports two filters:

| Filter           | What it does                                                |
| ---------------- | ----------------------------------------------------------- |
| **ID Whitelist** | *Only* allows the IDs you list. Everything else is blocked. |
| **ID Blacklist** | Blocks the IDs you list. Everything else is allowed.        |

Pick whichever makes a shorter list. Want it to work in 2 servers out of 40? Use a whitelist. Want it everywhere except 2 servers? Use a blacklist.

## ⚠️ Always add "Ignore self" - the infinite loop trap

**This is the single most important thing on this page.**

Here's how people break things:

You build a feature: *"when a new message appears, reply to it."*

{% stepper %}
{% step %}

## Someone sends a message

Your feature replies.
{% endstep %}

{% step %}

## Your reply is also a new message

The New Message event fires again - for your own reply.
{% endstep %}

{% step %}

## Your feature replies to its own reply

Go to step 2. Forever.
{% endstep %}
{% endstepper %}

That's an **infinite loop**, and your account is the one sending all those messages. It's the fastest way to get rate-limited.

**The fix takes five seconds:** add the **Ignore self** filter on the `user` parameter. Now your feature skips anything you sent yourself, and the loop can never start.

{% hint style="success" %}
**Every feature that sends, replies, edits, or reacts should have "Ignore self" on it.** Add it first, before you build anything else. Make it a habit.
{% endhint %}

### What about "Only self"?

Some features are meant to react to **you** - you type a keyword and something happens. Those use **Only self** instead, which allows *only* your own messages and blocks everyone else.

But be careful, because **"Only self" does not protect you from the loop** - it does the opposite. It means your own messages are the ones that trigger the feature, which is exactly the situation the loop needs.

If you're using **Only self**, the thing that has to break the loop is your **conditions**: the feature must only fire on something your own output can't match.

{% hint style="success" %}
**Safe:** trigger when the message is exactly `!ping`, reply with `pong`. `pong` isn't `!ping`, so the loop stops after one step.
{% endhint %}

{% hint style="danger" %}
**Dangerous:** trigger when the message contains `hello`, reply with `hello there`. Your reply contains `hello`, so it triggers itself - loop.
{% endhint %}

{% hint style="warning" %}
**The short version:** every feature gets either **Ignore self** or **Only self**. "Ignore self" ends the loop by itself. "Only self" means *you* are responsible for making sure your feature can't re-trigger on its own output.
{% endhint %}

## What happens when a parameter isn't there

From [Events and Parameters Page](/custom-features-tab/understanding-events-and-parameters): not every parameter exists on every event. A DM has no `guild` and no `member`.

So what does a filter do when the thing it's supposed to check isn't there? It follows one simple, consistent rule:

{% hint style="info" %}
**"Only" and "Whitelist" filters block. "Ignore" and "Blacklist" filters allow.**
{% endhint %}

The logic is that each filter fails in its *safe* direction. A whitelist exists to be restrictive, so when it can't confirm a match it says no. A blacklist exists to remove specific things, so when there's nothing to remove it says yes.

Written out:

| Filter                                                      | Parameter missing → |
| ----------------------------------------------------------- | ------------------- |
| Only self / Only bots / Only friends                        | ❌ Blocks            |
| Only in servers / Only in DMs                               | ❌ Blocks            |
| ID Whitelist *(any parameter)*                              | ❌ Blocks            |
| Ignore self / Ignore bots / Ignore friends / Ignore blocked | ✅ Allows            |
| ID Blacklist *(any parameter)*                              | ✅ Allows            |

This matters in practice. A **server ID whitelist** blocks DMs - there's no server to match, so nothing gets through. A **server ID blacklist** allows DMs - there's no server to block. Same list of IDs, opposite result in DMs.

{% hint style="info" %}
If you want a feature to work in servers *and* DMs, don't reach for a server whitelist. Use a channel whitelist instead - channel IDs exist everywhere, including DMs and group DMs.

You can also use the "Channel Type" condition.
{% endhint %}

## Combinations that work well

<table><thead><tr><th width="254">Goal</th><th>Filters to use</th></tr></thead><tbody><tr><td>Reply to people without looping</td><td><strong>Ignore self</strong> + <strong>Ignore bots</strong></td></tr><tr><td>Only work in one channel</td><td><strong>Ignore self</strong> + channel <strong>ID Whitelist</strong></td></tr><tr><td>Only work in your own servers</td><td><strong>Ignore self</strong> + server <strong>ID Whitelist</strong></td></tr><tr><td>Personal command you type yourself</td><td><strong>Only self</strong> <em>(plus a condition your output can't match) + Global execution limit (1 per 5 seconds)</em></td></tr><tr><td>Ignore one annoying person everywhere</td><td>user <strong>ID Blacklist</strong></td></tr><tr><td>React to bot announcements only</td><td><strong>Only bots</strong> + channel <strong>ID Whitelist</strong></td></tr><tr><td>DM auto-reply</td><td><strong>Ignore self</strong> + <strong>Ignore bots</strong> + <strong>Only in DMs</strong></td></tr><tr><td>Weekday-only feature</td><td><strong>Ignore self</strong> + <strong>Run on Weekdays</strong></td></tr></tbody></table>

Start with **Ignore self**, add **Ignore bots**, then narrow the place down with a whitelist. That covers the large majority of features.


# My values

**My Values are like variables in programming** - but you don't need to know what that means, because the everyday version is simpler.

{% hint style="info" %}
**A My Value is a labelled box.** You write a name on the outside, and you put a piece of information inside. Later, whenever you write that name, you get back whatever's in the box.
{% endhint %}

Or think of it as a sticky note: the label is the name, what's written on it is the value.

You give your value a name, put something in it, and use it later in your expressions.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FGJBARu6LDu1aLYMasDW9%2Fimage.png?alt=media&amp;token=a7e7724c-6fc9-4919-bff6-61d2997cfdff" alt=""><figcaption></figcaption></figure>

## Why bother

Three reasons, and they're all about saving yourself pain.

**You work something out once and reuse it.** If you need the same piece of information in four different actions, build it once into a My Value and point all four at that. No copying.

**You change it in one place.** Later you decide the greeting should use nicknames instead of usernames. With a My Value, you edit one box. Without one, you hunt through every action and hope you got them all.

## Creating one

My Values live **inside a** [**condition collection**](/custom-features-tab/conditions) - the **My Values** section at the top of it.

Each one needs three things:

| Field     | What it is                                                                                     |
| --------- | ---------------------------------------------------------------------------------------------- |
| **Name**  | The label on the box. How you'll refer to it later                                             |
| **Type**  | <p>What kind of thing it holds. Usually text<br>Nighty can usually detect it automatically</p> |
| **Value** | What goes in the box                                                                           |

The value isn't limited to fixed text you type. It can be built from the current event - the sender's name, the message's content, the server's name - the same way action details can. That's the point: you're capturing something *about this particular event* and giving it a name.

## When it gets filled in

A collection's My Values are worked out **before that collection's conditions are checked** - and they're worked out **whether or not the conditions end up passing**.

That means the same value is available everywhere downstream:

* The collection's own **conditions**
* Its **If met** actions
* Its **Otherwise** actions
* Every **later collection**
* The feature's **final actions**

So a My Value created in the first collection is still there at the very end of the run.

## Using one

You use a My Value in an expression by its name, written as **`my`**→**`your_name`**.

Anywhere you can drop in a live value - message text, a condition's comparison, a channel name - you can drop in a My Value.

## Changing one

{% hint style="info" %}
If you don't have the value to store yet, you can keep it empty and assign a value later, using the Edit My Value action.
{% endhint %}

Use the **Change one of My Values** action.

It takes the name of an existing value and gives it a new one.

{% stepper %}
{% step %}

## The value has to already exist

This action can only *change* a value, not create one.

Create it in a condition collection first - even if you just start it off empty.
{% endstep %}

{% step %}

## Only the contents change

The name and type stay exactly as you declared them. You're swapping what's in the box, not relabelling it.
{% endstep %}

{% step %}

## The new value is worked out fresh every time

If the new value is built from something live, it's recalculated at the moment the action runs - not reused from earlier.
{% endstep %}
{% endstepper %}

## How long they last

**One run.** That's it.

Every time your feature is triggered, its My Values start over from scratch. A value you set while handling one message is gone by the time the next message arrives.

That surprises people who expect a counter to keep counting. It won't.

| You want to…                           | Use                      |
| -------------------------------------- | ------------------------ |
| Hold onto something **during** one run | **My Values**            |
| Remember something **between** runs    | **Add ID Record** Action |


# Conditions

[Filters](/custom-features-tab/filters) were the bouncer at the door - broad checks about *where* and *who*. **Conditions are the detailed inspection** that happens next.

This is where you ask specific questions about what actually happened: does the message contain this word? Does this person have that role? Is this channel named something in particular?

{% hint style="info" %}
**Your goal with conditions is simple: make sure only the events you actually care about reach your actions.**
{% endhint %}

## 1. The condition collection

Conditions don't sit loose in your feature - they live inside a **condition collection**.

Each collection contains four things:

| Part                  | What it's for                                               |
| --------------------- | ----------------------------------------------------------- |
| **My Values**         | Values you work out once and reuse. Covered on its own page |
| **Conditions**        | The checks themselves, optionally grouped                   |
| **If met** actions    | Actions that run when the checks pass                       |
| **Otherwise** actions | Actions that run when the checks fail                       |

**The most basic setup is: add your conditions, and leave everything else untouched.** The defaults do the sensible thing - if the conditions pass, your feature carries on to its actions; if they don't, it stops.

That covers most features. The "If met / Otherwise" parts are there for when you want something more interesting, and they're explained in [section 6](#id-6.-advanced-if-met-otherwise).

You can have **more than one collection**. They're checked in order, top to bottom. For a first feature, one is plenty.

## 2. Every condition must pass

By default, when you add conditions, **all of them have to pass** before the event moves on to your actions. One failure and the feature stops there.

{% hint style="info" %}
If a condition runs into an error - for example it checks a message that isn't there - it counts as **not passed**, and the reason is written to your feature's log.
{% endhint %}

## 3. AND and OR - the connector

Conditions are joined by a **connector**.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FHKFP0WMI0TweH3ZQnNfl%2Fimage.png?alt=media&amp;token=a1e2cf4d-bf88-4221-a531-cba14ea16188" alt=""><figcaption></figcaption></figure>

It's set to **AND** by default, and clicking it switches it to **OR**.

The difference is the whole game:

| Connector | Passes when                                             |
| --------- | ------------------------------------------------------- |
| **AND**   | **Every** condition passes. All of them, no exceptions. |
| **OR**    | **At least one** condition passes. The rest can fail.   |

**AND makes your feature pickier.** Each condition you add narrows it further.

**OR makes it more accepting.** Each condition you add is another way in.

### AND example

> Message contains **"invoice"**\
> **AND**\
> Message has an attachment

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FQi61zZVuxqANoc4H37d9%2Finv1.jpg?alt=media&amp;token=92207ea0-4c44-4aa1-9efc-9963efebfa8e" alt=""><figcaption></figcaption></figure>

This only fires for messages that have *both* - the word *and* a file. A message saying "Here's the invoice" with nothing attached doesn't pass. Neither does a random screenshot with no text.

### OR example

> Message contains **"invoice"**\
> **OR**\
> Message has an attachment

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FKYzpFpOkjTmA0EAxusE6%2Finv2.jpg?alt=media&amp;token=ddf12e9a-5db6-43af-856d-e69ac811152d" alt=""><figcaption></figcaption></figure>

This fires if *either* is true. You usually want AND in such case.

{% hint style="warning" %}
**The connector applies to the whole group, not to one pair of conditions.** Switching one connector switches them all in that group. If you need *some* conditions joined by AND and others by OR, that's what groups are for.
{% endhint %}

## 4. Groups

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FZlsQSz0T6Pk5bKTbvL5U%2Fimage.png?alt=media&amp;token=825982ca-bca5-40a5-b2e2-2ea30d7b32fe" alt=""><figcaption></figcaption></figure>

A group is a set of conditions with its own connector. **You can have up to two groups per collection.**

That gives you three connectors to set:

* The connector **inside group 1**
* The connector **inside group 2**
* The connector **between the two groups**

This is what lets you build things a single flat list can't express.

### Why you need them

Say you want: *messages about deals, but only in the deals channels.*

Written as one flat list, it's impossible - you'd need "OR" between the deal words and "AND" between the words and the channels, and a flat list only has one connector.

With groups it's straightforward:

> **Group 1** *(OR)*\
> Message contains **"sale"**\
> OR Message contains **"discount"**
>
> **— AND —**
>
> **Group 2** *(OR)*\
> Channel is **#deals**\
> OR Channel is **#promos**

Read it out loud: *"(sale or discount) and (deals or promos)."* A "sale" message in #general doesn't pass. A "hello" message in #deals doesn't pass. Only a deal word in a deal channel gets through.

### Another example

*"Tell me about messages that mention me from friends, and about anything marked urgent regardless of who sent it."*

> **Group 1** *(AND)*\
> Message mentions me\
> AND Message is from a user on my whitelist
>
> **— OR —**
>
> **Group 2** *(AND)*\
> Message contains **"urgent"**

Read it out loud: *"(mentions me and is from a friend) or (contains urgent)."*

### The trick for getting it right

**Say your rule out loud in plain English first**, then put the brackets where you naturally pause. Whatever's inside a bracket becomes a group, and the word at the pause becomes the connector between them.

* *"(A or B) and C"* → group 1 holds A and B joined by OR; group 2 holds C; between them, AND
* *"A and (B or C)"* → group 1 holds A; group 2 holds B and C joined by OR; between them, AND

## 5. What happens when conditions pass

Nothing dramatic - the event moves on to the next collection, and once all the collections are done, to your feature's **final actions**.

That's the whole basic flow:

**Event → Filters → Conditions → Actions**

If you never touch If met / Otherwise, that's all conditions ever do: let events through, or stop them.

## 6. Advanced - If met / Otherwise

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FCV6uxF3I30oSLt6TEHSH%2Fadvanced.jpg?alt=media&amp;token=0eed9e6b-7536-419d-a8b4-a561c93bc486" alt=""><figcaption></figcaption></figure>

Here's where a feature stops being a straight line.

**By default, when your conditions aren't met, the feature just stops.** Nothing runs. That's usually what you want.

But each collection can also carry its own actions, which run *right there*, before the feature's final actions:

* **If met** actions run when the collection **passes**\
  ![](https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F2YUJDnTTzm7K73jLKj0L%2Fimage.png?alt=media\&token=fa697676-ca9c-4d07-ad41-b0722925f751)
* **Otherwise** actions run when the collection **fails**\
  ![](https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FJNlBxk4GTG1qGhIJHCET%2Fimage.png?alt=media\&token=2a3d7ba9-83cf-4505-be2b-7f2f287ac672)

And each side has a setting for what happens **afterwards**.

{% hint style="info" %}
**The most important rule here:** the action block **always runs**. If the collection passes, its "If met" actions run - full stop. The flow setting only decides what happens *after* those actions have finished. It never cancels them.
{% endhint %}

### After "If met", you can choose:

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FaOq1lbcXCMzL23qBj6jB%2Fimage.png?alt=media&amp;token=432a3aeb-2e8a-467f-b9c8-c5d0b29fe4ed" alt=""><figcaption></figcaption></figure>

| Option                        | What happens next                                                               |
| ----------------------------- | ------------------------------------------------------------------------------- |
| **Continue** *(default)*      | Move on to the next collection, as normal                                       |
| **Continue to final actions** | Skip any remaining collections and jump straight to the feature's final actions |
| **Stop**                      | End the feature here. The final actions do **not** run                          |

### After "Otherwise", you can choose:

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FUyTAR6ITez5XYNZ9saqO%2Fimage.png?alt=media&amp;token=1962ae85-362a-43f9-bdae-ffa40997c1bf" alt=""><figcaption></figcaption></figure>

| Option               | What happens next                                      |
| -------------------- | ------------------------------------------------------ |
| **Stop** *(default)* | End the feature here. The final actions do **not** run |
| **Continue**         | Move on to the next collection anyway                  |

### Example 1 - a fallback message

*"Only let approved people use this, and tell everyone else no."*

> **Collection 1**\
> Condition: user is on my approved list\
> **If met:** *(nothing)* → Continue\
> **Otherwise:** reply "Sorry, you're not on the list" → Stop
>
> **Final actions:** do the actual thing

Approved people sail past the collection and reach the final actions. Everyone else gets the reply and stops. Without the "Otherwise" block they'd just get silence.

### Example 2 - a command router

This is the pattern that makes multiple collections worth using.

> **Collection 1**\
> Condition: message is exactly `!weather`\
> **If met:** send the weather → **Stop**\
> **Otherwise:** → **Continue**
>
> **Collection 2**\
> Condition: message is exactly `!time`\
> **If met:** send the time → **Stop**\
> **Otherwise:** → **Stop**

One feature, two commands. Each collection checks for its own command, does its job, and stops so the next one never runs. The last "Otherwise → Stop" catches everything that matched neither.

Note that "Otherwise → Continue" in collection 1 is doing real work: it's what lets a `!time` message get past the weather check to reach collection 2. With the default (Stop), collection 2 would never be reached.

### Example 3 - a shortcut for VIPs

> **Collection 1**\
> Condition: user is on my VIP list\
> **If met:** send a personal greeting → **Continue to final actions**\
> **Otherwise:** → **Continue**
>
> **Collection 2**\
> Conditions: the normal checks everyone else has to pass\
> **If met:** → Continue\
> **Otherwise:** → Stop
>
> **Final actions:** log it to my private channel

VIPs get their greeting and jump straight to the logging, skipping the checks they don't need to pass. Everyone else goes through collection 2 the normal way. Both paths end at the same final actions, so the logging happens either way.

That's the difference between **Stop** and **Continue to final actions**: Stop ends everything, while Continue to final actions skips the remaining *checks* but still does the *work* at the end.

## 7. Two things to know

{% hint style="info" %}
**If met / Otherwise actions count as a run.** As soon as one of those blocks actually has actions in it and gets executed, it counts against your [execution limits](/custom-features-tab/execution-limits), the same as a normal run would. A collection that passes but has an empty "If met" block doesn't count for anything on its own.
{% endhint %}

{% hint style="info" %}
**My Values are ready before the checks run.** Any My Values in a collection are worked out first, before its conditions are evaluated - and they're worked out whether the collection ends up passing or failing. So your conditions, your "If met" actions and your "Otherwise" actions can all use them. They get their own page.
{% endhint %}


# Safe mode

**Safe mode lets you test a feature without it doing anything to Discord.**

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FSr6SbarX5fwJJX9QDRbs%2Fimage.png?alt=media&amp;token=a7de4a18-68ba-485e-877c-7b8fb356ddff" alt=""><figcaption></figcaption></figure>

{% hint style="danger" %}
It is **enabled by default** on every new feature, and that default is there for a reason.
{% endhint %}

## What it does

Your feature runs completely normally. The event fires, filters are checked, conditions are evaluated - everything behaves exactly as it will in real use.

Then, at the very moment it's time to carry out your actions, Nighty **stops** and instead shows a notification in the **overview** tab telling you that your actions were about to run.

Nothing is sent. Nothing is deleted. Nobody is banned. You just get told it *would* have happened.

{% hint style="info" %}
**This is a dress rehearsal with real events.** You can point a feature that bans people at a real server and watch it decide who it would ban, without anyone being banned.
{% endhint %}

It covers **every** action in your feature - the final actions, and the "If met" and "Otherwise" actions inside your condition collections. Nothing slips through.

## How to actually test with it

Turn the feature on with safe mode enabled, then **go and live your normal Discord life** for a while. Watch the notifications in the overview tab.

You're looking for one thing: **does it fire when it should, and stay quiet when it shouldn't?**

### ✅ What good looks like

Notifications appear only in the situations you built the feature for. You send a test message that should trigger it - a notification. You chat normally - silence.

### 🚨 What bad looks like

**Notifications are spamming.**

{% hint style="warning" %}
**If safe mode is producing a stream of notifications, something is wrong.** Your feature is triggering far more often than you intended. Turn safe mode off now and it would be spamming Discord, hitting rate limits, or acting on people you never meant to touch.
{% endhint %}

Spam is not a safe-mode problem - it's your feature telling you it's misconfigured, in the only way that can't hurt you. Treat it as the warning it is.

### What spam usually means

| Symptom                                        | Likely cause                                         |
| ---------------------------------------------- | ---------------------------------------------------- |
| Fires on **every** message                     | No conditions, or conditions that almost always pass |
| Fires on things you sent yourself              | Missing **Ignore self** filter                       |
| Fires in servers or channels you didn't intend | No whitelist filter                                  |
| Fires on bot messages                          | Missing **Ignore bots** filter                       |
| Fires several times per event                  | Your conditions match more broadly than you think    |
| Fires steadily forever                         | A loop - it's re-triggering on its own output        |

Fix the cause, then watch again. Repeat until it's quiet.

## Turning it off

{% hint style="success" %}
✅ **Only disable safe mode once you have fully tested the feature.**
{% endhint %}

Before you do, run through this:

* [ ] It fired **every** time it should have
* [ ] It stayed quiet every time it shouldn't have
* [ ] The notifications weren't spamming
* [ ] It has **Ignore self**&#x20;
* [ ] It has a sensible execution limit
* [ ] Its filters restrict it to the servers and channels you intended
* [ ] You've read the feature's log and there are no errors in it
* [ ] You're comfortable with what happens if it fires when you didn't expect - especially for anything irreversible: ban, kick, delete, purge, leave

If any of those is a no, leave safe mode on and keep working.

### Straight after turning it off

Watch the first few real runs closely and **check the log**. This is the first time your action expressions actually get worked out, so it's the first chance for a mistake in them to surface.

If anything looks wrong, turn safe mode back on or disable the feature immediately. You can toggle it as often as you like.\
For emergency: close Nighty and contact support.


# Actions

Filters let the event in. Conditions confirmed it's the one you wanted. **Actions are where you finally tell Nighty what to do about it.**

## Actions run in order, top to bottom

Your actions are a list, and Nighty works through it **one at a time, from top to bottom**. Each action finishes completely before the next one starts.

So the order you put them in is the order they happen in. If you send a message and then react to it, the message exists by the time the reaction happens. Flip them around and it doesn't.

Three things that follow from this:

**If one action fails, the rest still run.** Say your second action tries to delete a message that's already gone. It fails, the reason goes into your feature's log, and Nighty moves straight on to the third action. One broken step doesn't throw away the whole list.

**Slow actions hold up the queue.** Anything that takes time - waiting, fetching, running a slash command - blocks the actions below it until it's done. Usually that's exactly what you want, and it's what makes the **Wait** action useful.

## Using the result of an action

Some actions hand you something back that later actions can use - an Action Ref:

This unlocks a lot. Once **Send Message** hands you the message it sent, later actions can react to it, edit it, pin it, or delete it - all in the same run.

**Example - a self-deleting notice:**

{% stepper %}
{% step %}

## Send Message

"Cleaning up in 10 seconds"\
Expose an action ref
{% endstep %}

{% step %}

## Wait

10 seconds
{% endstep %}

{% step %}

## Delete

The message from step 1.
{% endstep %}
{% endstepper %}

**Example - post an invite:**

{% stepper %}
{% step %}

## Create Invite

Expose a ref to the invite
{% endstep %}

{% step %}

## Send Message

Include the invite's link in the text using Expressions.
{% endstep %}
{% endstepper %}

Remember the ordering rule: you can only use a result **after** the action that produced it. Put them in the right order.


# Expressions

An **expression** is a block you drop into a text input that gets **replaced with real information** when your feature runs.

You write it once. It comes out different every time, because it's filled in from whatever just happened.

Expressions are built from two things: **values** and **functions**.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F1D5SK9eWkdZiXkgOat01%2Fimage.png?alt=media&amp;token=7cd7e5d3-c333-4bb5-8632-6febb0e7e961" alt=""><figcaption></figcaption></figure>

## Values - the basic case

Here's the example that makes it obvious.

You want to reply to whoever sent a message and **@mention them**.

You add a Reply action and start typing: `Hello,` … and then you're stuck. You can't type the @mention, because it's a different person every time. There's nothing to type.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FP5eMTG6pu1wvwqWswxah%2Fimage.png?alt=media&amp;token=d225dba2-fb56-4f8a-a5d3-63ce66d6248a" alt=""><figcaption></figcaption></figure>

**That's what an expression is for.**

Instead of typing a name, you insert the value **user → mention**.&#x20;

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FazJ9v80YQisEDhQJIHrB%2Fimage.png?alt=media&amp;token=af39e5f0-ebb6-4e6f-9bf4-5beb867cb419" alt=""><figcaption></figcaption></figure>

Your reply now reads:

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FeD9zqNXK0jr1j5QHkr9W%2Fimage.png?alt=media&amp;token=ea2433c7-09aa-4112-a11f-0c9df5c281ab" alt=""><figcaption></figcaption></figure>

When the feature runs, <mark style="color:blue;">user→mention</mark> is swapped out for a real @mention of whoever actually sent the message. One reply action, correct for everybody.

Values come from the parameters you already know ([Events and Parameters Page](/custom-features-tab/understanding-events-and-parameters)): the message's content, the server's name, the channel, the member's nickname, your own account. Anything listed under a parameter can go into an expression.

You can mix plain text and values freely.

## Functions - doing something to a value

A value gives you information exactly as it is. **A function changes it, or works something out from it.**

{% hint style="info" %}
**Think of a function as a little machine.** You put something in one end, it does one job, and hands you back a result.
{% endhint %}

Every function has:

* a **name** - what job it does
* one or more **inputs** - what it works on
* a **result** - what comes back

The simplest example is `upper`, which makes text capital letters:

You select:

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FttvP2ajRfoteXodOYHvh%2Fimage.png?alt=media&amp;token=99202f8f-3c1a-4f42-99c6-11ccd4810fde" alt=""><figcaption></figcaption></figure>

You get: `HELLO`

On its own that's useless - you already knew it said "hello." Functions become useful when you **put a value inside them.**

By selecting:

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FbsDMmEO2nqSyde8CJSU6%2Fimage.png?alt=media&amp;token=369711e6-690a-48d1-95cf-74eed4ea64b5" alt=""><figcaption></figcaption></figure>

You get: `JOHNSMITH`  (whatever the users' name is).

Now it works on whoever triggered the event.

### Functions can go inside functions

The result of one function can be the input to another. That's where real power comes from.

Say you want someone's nickname, but not everyone has one, and you want it in capitals:

{% stepper %}
{% step %}

## Use upper function

Instead of writing text or selecting a value, select a "default" function.
{% endstep %}

{% step %}

## Get their nickname or display name

Their nickname, or their display name if they don't have one.
{% endstep %}
{% endstepper %}

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FI6cCVB6hDgwxIfnM5v0V%2Fimage.png?alt=media&amp;token=760a37b9-bacc-4f86-866f-a928283ef536" alt=""><figcaption></figcaption></figure>

Read it from the inside out, like a set of nesting boxes. The innermost thing is worked out first, and each layer wraps around the result.

## Things worth knowing

**Everything becomes text when combined.** When you mix values, functions and typed text into one field, the result is a single piece of text. A number used in a message comes out as text - that's fine and expected.

**Files are a special case.** Some values aren't text at all - a user's avatar, a message's attachments. When you put one of those into a field on its own, it's handed over as a real file. They are meant to be used in special File inputs.


# Slash command responder

**Run Slash Command** is a special action: it runs a slash command for you, exactly as if you'd typed it into Discord yourself.

Anything you'd normally do by typing `/` and picking a command, a feature can do automatically.

It works a little differently from other actions, so it's worth understanding before you build with it.

## Why it needs a channel

Slash commands don't exist everywhere. **Discord decides which commands are available in which channel**, based on which bots are there and what permissions they have.

For example, the `/advice` command from a bot in one server simply doesn't exist in a different server that doesn't have that bot.

So the action always asks you **which channel** to run in - and to show you a list of commands to pick from, Nighty has to go and ask Discord *"what's available here?"*

That's the whole reason for the setup below.

## The recommended setup

Follow these four steps in order and everything else falls into place.

{% stepper %}
{% step %}

### Add a custom channel parameter

Before anything else, add a **custom parameter** of type **Channel**, pointing at **a real channel where you know the command works.**

Go to that channel in Discord, copy its ID, and use that.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Fz8quZgo5DIv72f6fvpc1%2Fimage.png?alt=media&amp;token=efa6f4d0-a713-45eb-94be-286b7988571e" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
This is the key move. Without a specific, real channel, Nighty has nothing to ask Discord about, and the command picker will be empty. You'll have to input the command and options by hand and make no mistakes.
{% endhint %}
{% endstep %}

{% step %}

### Point the builder at it

Add the **Run Slash Command** action, and in the slash command builder set **"Refers to"** to the channel parameter you just made.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FQ0IHdELkKWlyRdR2U75R%2Fimage.png?alt=media&amp;token=305b5bc1-7df5-4818-a3ab-073360eb7cf6" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Pick the command and fill in the options

Now the **slash command selector** works. Nighty scans that channel and shows you every command actually available in it.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FncFPYWywn1ljL3X1QYWt%2Fimage.png?alt=media&amp;token=350d2ffc-95c7-4b30-b468-f39535801448" alt=""><figcaption></figcaption></figure>

Choose your command from the list and fill in its options.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FkBaXyb18FnW08SS9a6iZ%2Fimage.png?alt=media&amp;token=6bb43df0-cac6-4a6f-b486-b5c8bdfa3005" alt=""><figcaption></figcaption></figure>

{% hint style="success" %}
**This is why the setup is worth it.** Pick from a list instead of typing command names and option names by hand - no guessing, no typos, and the options come with the right names and types already.
{% endhint %}
{% endstep %}

{% step %}

### Switch the channel, if you need to

Once the command is chosen and configured, you can **switch the reference to a different channel** - including a dynamic one like `channel` from the event.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FC7SvkooOhE9pyhUmruoE%2Fimage.png?alt=media&amp;token=f1dd3b31-00c9-4591-89ee-9fdf807326ed" alt=""><figcaption></figcaption></figure>

The command you picked stays selected. Only the destination changes.
{% endstep %}
{% endstepper %}

## Which setup do you need?

**Always the same channel** - you're done at step 3. Leave the reference pointing at your custom parameter. This is the simplest and most reliable setup, and it's what you want for scheduled commands on a Repeat Action event.

**Specific servers, but the channel varies** - do all four steps. Build the command against your known channel, then switch the reference to the event's `channel`.

But now the command could run anywhere the event fires, which isn't what you want. So **restrict it with** [**filters**](/custom-features-tab/filters):

* Server **ID Whitelist** - only the servers where that command exists
* Channel **ID Whitelist** - only the exact channels you want

{% hint style="warning" %}
**Don't skip the filters on this setup.** Without them, the feature will try to run the command in every channel the event reaches - including ones where it doesn't exist. Those attempts fail and clutter your log.
{% endhint %}

## Options can be dynamic

The options you fill in aren't limited to fixed text. They accept [**expressions**](/custom-features-tab/expressions), the same as any other action field.

So a command's option can be built from the event:

* A `/ban` command with the reason set to `Triggered by user`→`displayName`
* A `/search` command with the query taken from `message`→`content`
* A `/give` command with an amount from a My Value

That's what turns this from "run one fixed command" into something that reacts to what actually happened.


# API Reference

{% hint style="info" %}
This documentation is a work in progress. UI scripting is a new feature, and additional content is still being added.
{% endhint %}

UI Scripting allows you to use the Nighty scripting engine to write Python scripts that dynamically update Nighty's user interface.

To get started, navigate to the [**Tab** ](/ui-scripting/api-reference/tab)section.

***

**Note**: The content below is dynamic, meaning you'll be directed here from other sections of the documentation when specific attributes or parameters are referenced.

## **Shared Attributes & Parameters**

A collection of commonly used properties that apply to multiple UI components, ensuring consistent styling and behavior across your layouts.

### Gap

Sets the spacing between child elements. Accepts a value from `0` to `10`, where `0` removes the gap and `10` applies the maximum spacing.

Examples:\
`gap=0`\
`gap=1`\
`gap=6`\
`gap=10`

***

### Margin

The `margin` option adds extra spacing around or inside a UI element. It accepts [Tailwind CSS margin classes](https://tailwindcss.com/docs/margin) ranging from `0` to `10`. \
\
Examples:

* `margin="m-4"`: Adds a margin of 4 units to all sides.
* `margin="m-0"`: Removes all margins.
* `margin="mr-6"`: Adds a margin of 6 units to the right side.
* `margin="mx-3"`: Adds a margin of 3 units to both the left and right sides.

Note: Not all prefixes are available!

All available prefixes: `m- mt- mr- mb- ml- mx- my-`&#x20;

***

### Visible

Controls the visibility of the UI element. When set to `True`, the element is displayed; when set to `False`, the element is hidden from the UI.

Examples:&#x20;

* `visible=True`
* `visible=False`


# Tab

Represents a Tab in the User Interface

## <mark style="color:green;">class</mark> **Tab**

#### Constructor

```python
# Only required parameters
tab = Tab(name="Custom Tab")

# All parameters
tab = Tab(
    name="Custom Tab",
    icon="users",
    gap: 8,
    title="My Custom Tab"
)
```

<table><thead><tr><th width="381">Parameter</th><th>Description</th></tr></thead><tbody><tr><td>name</td><td><p>Allowed: <code>string</code><br></p><p>Must be <strong>unique</strong> across the whole UI, cannot be longer than 36 characters</p></td></tr><tr><td>icon </td><td><p>Allowed: <code>string</code><br><br>Icon tab </p><p>Available icons: <a data-mention href="#icons">#icons</a></p></td></tr><tr><td>gap </td><td>See <a href="/ui-scripting/api-reference#gap">Gap</a><br>Default: <code>6</code><br><br>Spacing between <a href="/ui-scripting/api-reference/cardcontainer">CardContainer</a> elements. In most cases you can omit this.</td></tr><tr><td>title </td><td><p>Allowed: <code>string | None</code><br>Default: <code>None</code></p><p></p><p>Displayed at the top of your custom tab, above the cards.</p></td></tr></tbody></table>

#### Methods

<table><thead><tr><th width="257">Method</th><th>Description</th></tr></thead><tbody><tr><td>create_container(type, **kwargs)</td><td>Adds a <a href="/ui-scripting/api-reference/cardcontainer">CardContainer </a>to the Tab.</td></tr><tr><td>render()</td><td>Initializes the Tab. Should be called last, after all UI elements, cards, and containers are defined.</td></tr><tr><td>toast(title, description, type)</td><td><p>Displays a toast notification inside the app.<br><br><strong>Parameters</strong>:</p><ul><li><strong>Title</strong>: <code>string</code> | <em>required</em></li><li><strong>Description</strong>: <code>string</code> | <em>required</em></li><li><strong>Type</strong>: <code>"INFO"</code>, <code>"ERROR"</code>, <code>"SUCCESS"</code> |  <em>required</em></li></ul><p>Example:</p><pre class="language-python"><code class="lang-python">tab.toast(type="INFO", title="Hello, World!", description="Welcome from py!")
</code></pre><p></p><p><strong>Note</strong>: The toast will only display if the custom tab is open.</p></td></tr></tbody></table>

#### Icons

<table><thead><tr><th width="133">Image</th><th>String</th></tr></thead><tbody><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FkZ2M7R7MSPhT8o2hruT1%2FLayer%20139.png?alt=media&amp;token=d57429a9-a955-48dc-9920-45191b608190" alt="" data-size="original"></td><td>report</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FaasArhIUtR5gWerCsTLX%2FScreenshot%20(186)1.png?alt=media&amp;token=3382ecee-680f-4290-8f16-4337da1fc743" alt="" data-size="original"></td><td>chart</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FUig4r7BIO3WpDeOz8OIW%2FScreenshot%20(187)2.png?alt=media&amp;token=b66f91bf-2eae-4ba8-b75b-8f56e5c7d45e" alt="" data-size="original"></td><td>two_way</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FvY8f5bNYrHdUBBzWEqcD%2FScreenshot%20(188)3.png?alt=media&amp;token=2e7d10a0-c896-4cf3-910b-7dbd25d1c610" alt="" data-size="original"></td><td>inbox</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Ffs7QqJdhPj2EHgGDPIwg%2FScreenshot%20(189)4.png?alt=media&amp;token=5ded02a2-bb87-4173-a772-bfdc5cb8eae2" alt="" data-size="original"></td><td>star</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FvIt5fqBT8PT3CwE67Pji%2FScreenshot%20(190)5.png?alt=media&amp;token=e95848b3-f8c2-4cdb-af54-410a9a1a64f4" alt="" data-size="original"></td><td>search</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FTWIw5M90bsg9irHlnG0e%2FScreenshot%20(185).png?alt=media&amp;token=d974081e-b2b2-4ce8-9309-4a68ce75a913" alt="" data-size="original"></td><td>variants</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Ftnt6XIXGJa66HoLV2evh%2FScreenshot%20(191)6.png?alt=media&amp;token=a0e5c408-b8fa-40bd-b5cc-897bee91c997" alt="" data-size="original"></td><td>message</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FOXQyhQx5Zhg7PhX59Z8c%2FScreenshot%20(192)7.png?alt=media&amp;token=729805f2-dac8-40be-ab28-8a225f52b4af" alt="" data-size="original"></td><td>users</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FtCzsJHsw0cN3weNRPscQ%2FScreenshot%20(193)8.png?alt=media&amp;token=ba13fd64-b80c-4fb9-b3e4-3a44c6307220" alt="" data-size="original"></td><td>history</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FzDsFvwIIzgxteskO2iKi%2FScreenshot%20(194)9.png?alt=media&amp;token=74444008-f948-4241-8240-f787804d54b1" alt="" data-size="original"></td><td>clean</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FrJL88pWd80KVYvBYAlc4%2FScreenshot%20(195)10.png?alt=media&amp;token=bdb3b0a0-7a27-4dda-bb9c-f4b5e53d21a6" alt="" data-size="original"></td><td>magic</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FNSP8m7JXTmhklFQGIPL4%2FScreenshot%20(196)11.png?alt=media&amp;token=82adfea7-c6f0-4cdd-815c-6df3c374d9ca" alt="" data-size="original"></td><td>eraser</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FmJp14rUgkedN9yzF48o5%2FScreenshot%20(197)12.png?alt=media&amp;token=42774178-e15a-4310-93ef-6058b22c52a6" alt="" data-size="original"></td><td>convert</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FBwMIVYoVzJzL55jS2m63%2FScreenshot%20(198)13.png?alt=media&amp;token=397fa8d1-4a04-42ca-ba5e-85555971dc91" alt="" data-size="original"></td><td>cloud</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FI1arTNVMTs6y5xHCZiV4%2FScreenshot%20(199)14.png?alt=media&amp;token=d0fb0554-e88b-464d-897a-f182ba064f2a" alt="" data-size="original"></td><td>calc</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FFuDV2eeTnUSEFpN0e0KE%2FScreenshot%20(200)15.png?alt=media&amp;token=e7e12cc2-f7a9-4708-9943-3bf93ab4be58" alt="" data-size="original"></td><td>calendar</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FxEym7RTeK0jvlGJUL7Dj%2FScreenshot%20(201)16.png?alt=media&amp;token=e8acaf0c-d9f3-490f-8b78-5d2daf9c57e5" alt="" data-size="original"></td><td>key</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FnJXX8e1uJ6M4Ybq5Tcic%2FScreenshot%20(202)17.png?alt=media&amp;token=67da827f-9cec-488c-917a-c5a146f4bd83" alt="" data-size="original"></td><td>gift</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FfVGhXuGbfGlo3xeI2czz%2FScreenshot%20(203)18.png?alt=media&amp;token=7ca216cd-7f70-4424-bf3b-cf887c61956e" alt="" data-size="original"></td><td>trophy</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Fkwk0r8nMnkyTzZ5vaqp4%2FScreenshot%20(204)19.png?alt=media&amp;token=6e4d30b7-6cf4-466e-a052-986ba4ef11fd" alt="" data-size="original"></td><td>book</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FNjHiyEuxqlmtCfTPkdGc%2FScreenshot%20(205)20.png?alt=media&amp;token=13b08582-c2c4-4d38-9b27-a8fdc6f4aa3d" alt="" data-size="original"></td><td>sun</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FOmVw371Tk4Gs3RGb9SqA%2FScreenshot%20(206)21.png?alt=media&amp;token=3fe32c95-c536-460b-9863-d3d9c5f02259" alt="" data-size="original"></td><td>cart</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FA1wC5ISUR3plYg4QEuOa%2FScreenshot%20(207)22.png?alt=media&amp;token=e0e60cb3-916f-48c3-a945-0f37467afdc7" alt="" data-size="original"></td><td>pill</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FwkDQNGUYD9vQhjY5xQEt%2FScreenshot%20(208)23.png?alt=media&amp;token=eaa6b05b-33fe-4741-a220-b5ef3178ccce" alt="" data-size="original"></td><td>bookmark</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Ff7hi5bI4gko60BrjDnjD%2FScreenshot%20(209)24.png?alt=media&amp;token=8b81163a-3e3d-460c-8d5e-61bf5d82d2a0" alt="" data-size="original"></td><td>mail</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2Fqc1XajJuOZdweq3mpN2H%2FScreenshot%20(210)25.png?alt=media&amp;token=565cf2d9-61d8-4ea9-8e29-f670ffae15e8" alt="" data-size="original"></td><td>bear</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FOLwVm8vnmrUR7TvyfOjP%2FScreenshot%20(211)26.png?alt=media&amp;token=71c75c83-85f5-4449-8864-c8dc8f987824" alt="" data-size="original"></td><td>heart</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FvnUqaNuaFg8EQMyA7wIP%2FScreenshot%20(212)27.png?alt=media&amp;token=ae3e496e-a94f-4fbf-a77e-8acd2ffe96d0" alt="" data-size="original"></td><td>music</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FElbCt1VIYoEclObmJzHD%2FScreenshot%20(213)28.png?alt=media&amp;token=b2418314-b5d6-4940-9ea3-572457d18611" alt="" data-size="original"></td><td>umbrella</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FH2FoCMbycLxpdM9LgcYp%2FScreenshot%20(214)29.png?alt=media&amp;token=4135b36b-6b0a-4db7-a708-78a63a1eae45" alt="" data-size="original"></td><td>tube</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FOwPl9nt0H11tJ1l1cr62%2FScreenshot%20(215)30.png?alt=media&amp;token=f850015d-f050-4da9-affd-d2327c66d834" alt="" data-size="original"></td><td>palette</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F9x1RpCm2YRBNR2yYlkxk%2FScreenshot%20(216)31.png?alt=media&amp;token=8fa63476-ac14-4cee-a248-9761e81d7119" alt="" data-size="original"></td><td>lock</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FnE14eghDNFaM7x4Lsazj%2FScreenshot%20(217)32.png?alt=media&amp;token=43215e31-ac45-4628-a57f-53f6e6d8e860" alt="" data-size="original"></td><td>fire</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F6dNAkNDXB4CRcff38JkV%2FScreenshot%20(218)33.png?alt=media&amp;token=9a18a1cd-a491-46a4-abb4-b0e65e73419f" alt="" data-size="original"></td><td>preferences</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FzIQEpD3titS6kiyKj6P5%2FScreenshot%20(219)34.png?alt=media&amp;token=57aba2bc-b977-4836-8126-cae21868bf53" alt="" data-size="original"></td><td>power</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FWljDmQrj9uMM1YUoMDv0%2FScreenshot%20(221)36.png?alt=media&amp;token=b82e437b-026b-4754-86df-3723886af82b" alt="" data-size="original"></td><td>share</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FqIShaG4Pm1PahCOT1h0L%2FScreenshot%20(222)37.png?alt=media&amp;token=04f04a5e-5dca-4984-a5ea-c35d3d5a4eac" alt="" data-size="original"></td><td>bell</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FghQUAkVlBk570ttNTwJl%2FScreenshot%20(223)38.png?alt=media&amp;token=0c3f8873-492a-4244-a69c-43b23de1bd2b" alt="" data-size="original"></td><td>trash</td></tr><tr><td><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FXzt0EJ7c2j6BKTtOYbEp%2FScreenshot%20(224)0.png?alt=media&amp;token=d070c33e-f41b-4d71-b680-fecd1e143488" alt="" data-size="original"></td><td>nitro</td></tr></tbody></table>


# CardContainer

The CardContainer class is used to group single or multiple Card elements. Each container can either be of type columns or rows, which determines the layout of the cards.

## <mark style="color:green;">class</mark> **CardContainer**

{% hint style="warning" %}
Do not create **CardContainer** instances directly. Instead, use the `create_container()` method of the [Tab](/ui-scripting/api-reference/tab) class to genereate them.
{% endhint %}

{% hint style="info" %}
**At least one `CardContainer` is required within a `Tab` element.**

A `Tab` can hold multiple `CardContainer` objects. Nesting `CardContainer` objects inside each other is allowed, meaning you can create complex layouts with nested containers. However, we **do not recommend** nesting more than 3 `CardContainer` objects, as it may result in a poorly structured UI that looks disorganized or cluttered.
{% endhint %}

#### Constructor

```python
# Basic example
container = tab.create_container() # Default type of "columns" and variants of {"height": "full","width": "full"} will be used

# Custom parameters
container = tab.create_container(
    type="columns",
    height="full",
    width="auto",
    gap=8
)

# Rows example
container = tab.create_container(
    type="rows",
    height="full",
    width="auto",
    gap=8
)
```

<table><thead><tr><th width="216">Parameter</th><th>Description</th></tr></thead><tbody><tr><td>type</td><td>Allowed: <code>"columns"</code> | <code>"rows"</code><br>Default: <code>"columns"</code></td></tr><tr><td>width</td><td>Allowed: <code>"auto"</code> | <code>"full"</code><br>Default: <code>"full"</code></td></tr><tr><td>height</td><td>Allowed: <code>"auto"</code> | <code>"full"</code><br>Default: <code>"full"</code></td></tr><tr><td>gap</td><td>See <a href="/ui-scripting/api-reference#gap">Gap</a><br>Default: <code>6</code></td></tr></tbody></table>

#### Methods

<table><thead><tr><th width="279">Method</th><th>Description</th></tr></thead><tbody><tr><td>create_container(type, **kwargs)</td><td>Creates a nested CardContainer.</td></tr><tr><td>create_card(**kwargs)</td><td>Creates and adds the <a href="/ui-scripting/api-reference/card">Card </a>to the CardContainer.</td></tr></tbody></table>

### Examples

The following examples demonstrate how to create layouts using CardContainers and [Cards](/ui-scripting/api-reference/card).

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FcZMTAwZDuvemGXrjYxwD%2Fex3.png?alt=media&amp;token=e8856ec5-4f65-43ad-8776-e6541d9c3a55" alt=""><figcaption><p>Simple example with just one card</p></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

# this container holds only one card, type does not matter
container = tab.create_container(type="columns")
card = container.create_card()

tab.render()
```

***

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F6ASDzzDKPcCtHQSlq1zX%2Fex4.png?alt=media&amp;token=9b8203b8-1ac7-4d84-9695-aeab3a4ea43e" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

container = tab.create_container(type="columns")

left_card = container.create_card()
right_card = container.create_card()

tab.render()
```

***

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FWTubZL12loIRU1dARtPG%2Fex1.png?alt=media&amp;token=862d79d4-0aca-40c1-939e-539ef9617c61" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

top_container = tab.create_container(type="columns")

top_left_card = top_container.create_card()
top_right_card = top_container.create_card()

# this container holds only one card, type does not matter
bottom_container = tab.create_container(type="columns") 

bottom_card = bottom_container.create_card()


tab.render()
```

***

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F2BBDegsyehpzASi6A8mW%2Fex2.png?alt=media&amp;token=23aa5608-ba03-4681-9050-6b1d315048fb" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

top_container = tab.create_container(type="columns")

top_left_card = top_container.create_card()
top_right_card = top_container.create_card()

bottom_container = tab.create_container(type="columns")

bottom_left_card = bottom_container.create_card()
bottom_right_card = bottom_container.create_card()


tab.render()
```

***

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FaZ4Bq4iufsaIthg4kfn8%2Fex5.png?alt=media&amp;token=74436862-d471-4069-922b-fc6e52eac2c5" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

wrapper_container = tab.create_container(type="columns")

left_container = wrapper_container.create_container(type="rows")
top_card = left_container.create_card()
bottom_card = left_container.create_card()

# this container holds only one card, type does not matter
right_container = wrapper_container.create_container(type="columns")
right_card = right_container.create_card()

tab.render()
```


# Card

A Card is a container for UI elements, offering a background and space to place components like buttons, inputs, and text within a CardContainer.

## <mark style="color:green;">class</mark> **Card**

{% hint style="warning" %}
Do not create **Card** instances directly. Instead, use the `create_card()` method of the [CardContainer](/ui-scripting/api-reference/cardcontainer) class to genereate them.
{% endhint %}

{% hint style="info" %}
Nesting multiple Cards is prohibited.

Cards must be a direct children of the [CardContainer](/ui-scripting/api-reference/cardcontainer).
{% endhint %}

#### Constructor

```python
# Basic example
card = container.create_card() # Default type of "columns"

# Custom properties
container = tab.create_container(
    type="columns",
    height: "auto",
    width: "auto",
    vertical_align: "center",
    horizontal_align: "center",
    gap: 4,
    disallow_shrink=True
)

# Rows example
container = tab.create_container(type="rows")
```

| Parameter         | Description                                                                             |                                                                                                                                                                                                                                                                                                                                                                  |                                                                                                                                                                 |
| ----------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| type              | <p>Allowed: <code>"columns"</code>                                                      | <code>"rows"</code><br>Default: <code>columns</code></p>                                                                                                                                                                                                                                                                                                         |                                                                                                                                                                 |
| height            | <p>Allowed: <code>"auto"</code>                                                         | <code>"full"</code><br>Default: <code>"full"</code></p>                                                                                                                                                                                                                                                                                                          |                                                                                                                                                                 |
| width             | <p>Allowed: <code>"auto"</code>                                                         | <code>"full"</code><br>Default: <code>"full"</code></p>                                                                                                                                                                                                                                                                                                          |                                                                                                                                                                 |
| vertical\_align   | <p>Allowed: <code>"start"</code>                                                        | <code>"center"                                                                                                                                                                                                                                                                                                                                                   | "end"</code><br>Default: <code>"start"</code><br><br>Defines the alignment of the card's child elements (UI elements and Groups) along the vertical axis.</p>   |
| horizontal\_align | <p>Allowed: <code>"start"</code>                                                        | <code>"center"                                                                                                                                                                                                                                                                                                                                                   | "end"</code><br>Default: <code>"start"</code><br><br>Defines the alignment of the card's child elements (UI elements and Groups) along the horizontal axis.</p> |
| gap               | <p>See <a href="/ui-scripting/api-reference#gap">Gap</a><br>Default: <code>6</code></p> |                                                                                                                                                                                                                                                                                                                                                                  |                                                                                                                                                                 |
| disallow\_shrink  | <p>Allowed: <code>True                                                                  | False</code><br>Default: <code>False</code><br><br>Prevents the card from shrinking when its width or height is set to <code>auto</code>. Useful when cards adjust their size to fit other elements in the tab. Set to <code>True</code> to disable shrinking. Leave it as default if no issues occur, as forcing this may cause unexpected layout behavior.</p> |                                                                                                                                                                 |

#### Methods

| Method                                   | Description                                                                                      |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
| create\_group(type, \*\*kwargs)          | Creates a Group instance within the card to organize and manage child elements as a single unit. |
| create\_ui\_element(element, \*\*kwargs) | Creates a [UI Element](/ui-scripting/api-reference/ui-elements) within the card.                 |

### Examples

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FJ2QCBoNnDWxkuIvY02Ku%2Fex6.png?alt=media&amp;token=a73678e2-6dc8-4761-9012-dcea83df34bf" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

container = tab.create_container(type="columns")
card = container.create_card(gap=2)

card.create_ui_element(UI.Text, content="Hello, World!", size="xl")
card.create_ui_element(UI.Text, content="Welcome from py.", size="base", color="#d5e0e6")

tab.render()
```

***

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2F9tGnAIKdSfDg4jGFS6j2%2Fex7.png?alt=media&amp;token=c749b84a-b0c5-4787-adb6-83c280c197d9" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

container = tab.create_container(type="columns")
card = container.create_card(vertical_align="start", horizontal_align="center", gap=2)

card.create_ui_element(UI.Text, content="Hello, World!", size="xl")
card.create_ui_element(UI.Text, content="Welcome from py.", size="base", color="#d5e0e6")

tab.render()
```

***

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FL73pB4pnpwqQfLWt6sW2%2Fex8.png?alt=media&amp;token=c17c0d90-962b-4902-822d-1f580cf7e80d" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

container = tab.create_container(type="columns")
card = container.create_card(gap=2, vertical_align="center", horizontal_align="center")

card.create_ui_element(UI.Text, content="Input email", size="base")
email_input = card.create_ui_element(UI.Input, label="Email", margin="mb-4")
submit_button = card.create_ui_element(UI.Button, label="Submit", variant="cta")

def on_submit():
    print(f"{email_input.value} submitted")

submit_button.onClick = on_submit

tab.render()
```


# Group

## <mark style="color:green;">class</mark> **Group**

A `Group` is a UI element that can only exist as a child of a [Card](/ui-scripting/api-reference/card). It is used to organize multiple [UI Elements](/ui-scripting/api-reference/ui-elements) into a single unit for easier alignment and positioning.

{% hint style="warning" %}
Do not create **Group** instances directly. Instead, use the `create_group()` method of the [Card](/ui-scripting/api-reference/card) class to genereate them.
{% endhint %}

{% hint style="info" %}
Nesting multiple Groups is allowed.
{% endhint %}

#### Constructor

```python
# Basic example
group = card.create_group(type="columns")

# Setting extra attributes
group = card.create_group(
    type="columns",
    horizontal_align="center",
    vertical_align="center",
    gap=2,
    full_width=True
)

# Type of rows
group = card.create_group(type="rows")

# Nesting groups
group1 = card.create_group(type="columns")
group2 = group1.create_group(type="rows")
```

| Parameter         | Description                                                                              |                                                                                                                                                                                                                                                                           |                                                                                                                                                                  |
| ----------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| type              | <p>Allowed: <code>"columns"</code>                                                       | <code>"rows"</code><br>Default: <code>columns</code></p>                                                                                                                                                                                                                  |                                                                                                                                                                  |
| vertical\_align   | <p>Allowed: <code>"start"</code>                                                         | <code>"center"                                                                                                                                                                                                                                                            | "end"</code><br>Default: <code>"start"</code><br><br>Defines the alignment of the group's child elements (UI elements and Groups) along the vertical axis.</p>   |
| horizontal\_align | <p>Allowed: <code>"start"</code>                                                         | <code>"center"                                                                                                                                                                                                                                                            | "end"</code><br>Default: <code>"start"</code><br><br>Defines the alignment of the group's child elements (UI elements and Groups) along the horizontal axis.</p> |
| gap               | <p>See <a href="/ui-scripting/api-reference#gap">Gap</a>.<br>Default: <code>6</code></p> |                                                                                                                                                                                                                                                                           |                                                                                                                                                                  |
| full\_width       | <p>Allowed: <code>True                                                                   | False</code><br>Default: <code>False</code><br><br>Forces the group to occupy the entire available width. Use this if the group appears smaller than expected. Note: It may require setting the parent elements (such as containers and cards) to full width as well.</p> |                                                                                                                                                                  |

#### Methods

| Method                                   | Description                                                                       |
| ---------------------------------------- | --------------------------------------------------------------------------------- |
| create\_group(type, \*\*kwargs)          | Creates a nested Group instance within the current Group.                         |
| create\_ui\_element(element, \*\*kwargs) | Creates a [UI Element](/ui-scripting/api-reference/ui-elements) within the Group. |

### Examples

Improved version of the last example from the previous section (Card). Here, Group is used to place the button next to the input.

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FsoHBArW214LX1H77YD0w%2Fex9.png?alt=media&amp;token=bd7d2ba6-1371-45e0-b6d1-9e876d89a69f" alt=""><figcaption></figcaption></figure>

```python
tab = Tab(name='My Custom Tab')

container = tab.create_container(type="columns")

card = container.create_card(gap=2, vertical_align="center", horizontal_align="center")
card.create_ui_element(UI.Text, content="Input email", size="base")

group = card.create_group(type="columns", gap=2, vertical_align="center")

email_input = group.create_ui_element(UI.Input, label="Email")
submit_button = group.create_ui_element(UI.Button, label="Submit", variant="cta")

def on_submit():
    print(f"{email_input.value} submitted")

submit_button.onClick = on_submit

tab.render()
```


# UI Elements

The `UI` class provides a variety of versatile UI elements designed to enhance custom tabs. These elements can only be created within [Groups](/ui-scripting/api-reference/group) or [Cards ](/ui-scripting/api-reference/card)and must be their direct children.

Available UI Elements:

* UI.Text
* UI.Button
* UI.Input
* UI.Image
* UI.Select
* UI.Checkbox
* UI.Table


# Text

UI Element - Text

## <mark style="color:green;">class</mark> **UI.Text**

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FTRVn3pJklbICGkCJ01Az%2Fex10.png?alt=media&amp;token=7447fee6-95ca-44f3-a24b-cf4eecbdd8e1" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
**All attributes are read/write**: You can access (read) and modify (set) any attribute at any time, and the changes will immediately reflect in the UI.
{% endhint %}

```python
# Appending text to a card
text = card.create_ui_element(UI.Text,
    content="Hello, World!",
    size="sm",
    weight="bold",
    color="#FFFFFF",
    align="left"
)

# Appending text to a card, leaving the default settings
text = card.create_ui_element(UI.Text)

# Setting attributes
text.content = "Hello, World"
```

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>content</td><td><p>any <code>string</code><br></p><p>Note: use <code>\n</code> to create a new line.</p></td></tr><tr><td>size</td><td><p><code>"tiny"</code></p><p><code>"sm"</code></p><p><code>"base"</code></p><p><code>"lg"</code></p><p><code>"xl"</code></p><p><code>"2xl"</code><br><br>Default: <code>"base"</code></p></td></tr><tr><td>weight</td><td><p><code>"thin"</code></p><p><code>"light"</code></p><p><code>"normal"</code></p><p><code>"medium"</code></p><p><code>"bold"</code></p><p><code>"extrabold"</code><br><code>"black"</code><br><br>Default: <code>"normal"</code></p></td></tr><tr><td>color</td><td><p>Any CSS valid color value.</p><p></p><p>See: <a href="https://www.w3schools.com/cssref/css_colors_legal.php">CSS Color Values</a></p><p><br>Examples:<br><code>"#FFFFFF"</code><br><code>"black"</code><br><code>"rgb(255, 0, 0)"</code><br><code>"hsla(0, 100%, 50%, 0.5)"</code></p><p><br>Default: <code>"#FFFFFF"</code></p></td></tr><tr><td>align</td><td><p><code>"left"</code></p><p><code>"center"</code></p><p><code>"right"</code><br><br>Default: <code>"left"</code></p></td></tr><tr><td>margin</td><td>See <a href="/ui-scripting/api-reference#margin">Margin</a>.<br><br>Default: <code>"m-0"</code></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>


# Button

## <mark style="color:green;">class</mark> **UI.Button**

<figure><img src="https://3836407116-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FaFce5BqRsGvQObqcfG7q%2Fuploads%2FcZUCoVRIFjV6GRYuxHGH%2Fex11.png?alt=media&amp;token=568f30b9-7304-41e8-aa2d-abbc32124248" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
**All attributes are read/write**: You can access (read) and modify (set) any attribute at any time, and the changes will immediately reflect in the UI.
{% endhint %}

<pre class="language-python"><code class="lang-python"><strong># setting up click handler
</strong><strong>def click_handler():
</strong>    print('clicked')

# Creating basic button
card.create_ui_element(UI.Button, label="Click me!", onClick=click_handler)

# Setting attributes
button = card.create_ui_element(
    UI.Button,
    label="Click me!",
    variant="ghost",
    size="sm",
    color="danger",
    full_width=True,
    onClick=click_handler
)

##################################
# Example with asnychronus updates

button = card.create_ui_element(UI.Button, label="Submit")

async def click_handler():
    button.loading=True
    # Do some async tasks, for example fetch data from an API...
    button.loading=False
    # perform other updates

button.onClick = click_handler
</code></pre>

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>label</td><td>any <code>string</code></td></tr><tr><td>variant</td><td><p><code>"solid"</code></p><p><code>"bordered"</code></p><p><code>"ghost"</code></p><p><code>"light"</code></p><p><code>"flat"</code></p><p><code>"cta"</code><br><br>Default: <code>"solid"</code></p></td></tr><tr><td>color</td><td><p><code>"primary"</code></p><p><code>"default"</code></p><p><code>"success"</code></p><p><code>"danger"</code><br></p><p>Note: If <code>cta</code> variant is used, custom color will not be applied.</p><p><br>Default: <code>"primary"</code></p></td></tr><tr><td>size</td><td><p><code>"sm"</code></p><p><code>"md"</code><br><br>Default: <code>"md"</code></p></td></tr><tr><td>full_width</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>disabled</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>loading</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>margin</td><td>See <a href="/ui-scripting/api-reference#margin">Margin</a>.<br><br>Default: <code>"m-0"</code></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>

### Events

| Event   | Arguments | Description                           |
| ------- | --------- | ------------------------------------- |
| onClick | -         | Triggered when the button is clicked. |


# Input

## <mark style="color:green;">class</mark> **UI.Input**

{% hint style="info" %}
This documentation is a work in progress. UI scripting is a new feature, and additional content is still being added. Stay tuned for updates!
{% endhint %}

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>label</td><td>any <code>string</code><br><br>Default: <code>None</code></td></tr><tr><td>placeholder</td><td>any <code>string</code><br><br>Default: <code>None</code></td></tr><tr><td>value</td><td>The input value, as a <code>string</code><br><br>Default: <code>None</code></td></tr><tr><td>description</td><td>any <code>string</code><br><br>Default: <code>None</code></td></tr><tr><td>show_clear_button</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>invalid</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>disabled</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>readonly</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>required</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>full_width</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>error_message</td><td>any <code>string</code><br><br>Default: <code>None</code></td></tr><tr><td>margin</td><td>See <a href="/ui-scripting/api-reference#margin">Margin</a>.<br><br>Default: <code>"m-0"</code></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>

### Events

| Event   | Arguments        | Description                               |
| ------- | ---------------- | ----------------------------------------- |
| onInput | value (`string`) | Triggered when typing in the input field. |


# Image

## <mark style="color:green;">class</mark> **UI.Image**

{% hint style="info" %}
This documentation is a work in progress. UI scripting is a new feature, and additional content is still being added. Stay tuned for updates!
{% endhint %}

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>url </td><td>valid URL <code>string</code><br><br>This value is required.</td></tr><tr><td>alt</td><td>any <code>string</code><br><br>Default: <code>"Image"</code></td></tr><tr><td>circle</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>shadow</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>width</td><td><p><code>%</code>, <code>px</code> or <code>auto</code> width value.</p><p><br>Examples:<br><code>"50%"</code><br><code>"75px"</code><br><code>"auto"</code></p><p><br>Default: <code>"auto"</code></p></td></tr><tr><td>height</td><td><p><code>%</code>, <code>px</code> or <code>auto</code> height value.</p><p><br>Examples:<br><code>"50%"</code><br><code>"75px"</code><br><code>"auto"</code></p><p><br>Default: <code>"auto"</code></p></td></tr><tr><td>rounded</td><td><p><code>"none"</code></p><p><code>"sm"</code></p><p><code>"md"</code></p><p><code>"lg"</code></p><p><code>"xl"</code></p><p><code>"2xl"</code><br><br>Default: <code>"md"</code></p></td></tr><tr><td>fill_type</td><td><p><code>"cover"</code></p><p><code>"contain"</code><br><br>Default: <code>"contain"</code></p></td></tr><tr><td>border_color</td><td><p>Any CSS valid color value.</p><p></p><p>See: <a href="https://www.w3schools.com/cssref/css_colors_legal.php">CSS Color Values</a></p><p><br>Examples:<br><code>"#FFFFFF"</code><br><code>"black"</code><br><code>"rgb(255, 0, 0)"</code><br><code>"hsla(0, 100%, 50%, 0.5)"</code></p><p><br>Default: <code>"#FFFFFF"</code></p></td></tr><tr><td>border_width</td><td><code>integer</code> between <code>0</code> and <code>10</code><br><br>Default: <code>0</code></td></tr><tr><td>margin</td><td>See <a href="/ui-scripting/api-reference#margin">Margin</a>.<br><br>Default: <code>"m-0"</code></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>


# Toggle

## <mark style="color:green;">class</mark> **UI.Toggle**

{% hint style="info" %}
This documentation is a work in progress. UI scripting is a new feature, and additional content is still being added. Stay tuned for updates!
{% endhint %}

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>label</td><td>any <code>string</code></td></tr><tr><td>checked</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>disabled</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>

### Events

| Event    | Arguments                 | Description                           |
| -------- | ------------------------- | ------------------------------------- |
| onChange | checked (`True \| False`) | Triggered when the toggle is clicked. |


# Select

## <mark style="color:green;">class</mark> **UI.Select**

{% hint style="info" %}
This documentation is a work in progress. UI scripting is a new feature, and additional content is still being added. Stay tuned for updates!
{% endhint %}

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>label</td><td>any <code>string</code><br><br>This value is required.</td></tr><tr><td>items</td><td><p>a <code>list</code> of items to display.<br><br>Example:<br><code>[{</code></p><p><code>"id": "one",</code> </p><p><code>"title": "Item 1",</code></p><p><code>"iconUrl": "https://i.com/image.png"</code></p><p><code>}]</code><br></p><p><code>id</code>: <strong>Must be unique</strong>. Duplicate IDs can cause unexpected behavior and may result in incorrect UI rendering. Ensure each ID is distinct to avoid issues.</p><p></p><p><code>id</code> &#x26; <code>title</code> are required, <code>iconUrl</code> is optional</p><p><br>Default: <code>None</code></p></td></tr><tr><td>selected_items</td><td>a <code>list</code> of (item) ids to display.<br><br>Default: <code>None</code></td></tr><tr><td>disabled_items</td><td>a <code>list</code> of (item) ids to disable.<br><br>Default: <code>None</code></td></tr><tr><td>mode</td><td><p><code>"single"</code></p><p><code>"multiple"</code><br><br>Default: <code>"single"</code></p></td></tr><tr><td>description</td><td>any <code>string</code><br><br>Default: <code>None</code></td></tr><tr><td>loading</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>disabled</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>invalid</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>error_message</td><td>any <code>string</code><br><br>Default: <code>None</code></td></tr><tr><td>full_width</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>margin</td><td>See <a href="/ui-scripting/api-reference#margin">Margin</a>.<br><br>Default: <code>"m-0"</code></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>

### Events

| Event    | Arguments                | Description                           |
| -------- | ------------------------ | ------------------------------------- |
| onChange | selected\_items (`list`) | Triggered when the selection changes. |


# Checkbox

## <mark style="color:green;">class</mark> **UI.Checkbox**

{% hint style="info" %}
This documentation is a work in progress. UI scripting is a new feature, and additional content is still being added. Stay tuned for updates!
{% endhint %}

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>label</td><td>any <code>string</code><br><br>This value is required.</td></tr><tr><td>checked</td><td><p><code>True | False</code></p><p><br>Default: <code>False</code></p></td></tr><tr><td>disabled</td><td><p><code>True | False</code></p><p><br>Default: <code>False</code></p></td></tr><tr><td>invalid</td><td><p><code>True | False</code></p><p><br>Default: <code>False</code></p></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>

### Events

| Event    | Arguments                 | Description                             |
| -------- | ------------------------- | --------------------------------------- |
| onChange | checked (`True \| False`) | Triggered when the checkbox is toggled. |


# Table

## <mark style="color:green;">class</mark> **UI.Table**

{% hint style="info" %}
This documentation is a work in progress. UI scripting is a new feature, and additional content is still being added. Stay tuned for updates!
{% endhint %}

<table><thead><tr><th width="334">Attribute</th><th>Value</th></tr></thead><tbody><tr><td>columns</td><td><p>a <code>list</code> of columns (<code>dict</code>)<br><br><code>column</code> properties:<br>type: <code>"tag"</code>, <code>"text"</code> or <code>"button"</code><br>label: any <code>string</code><br>buttons: a <code>list</code> of buttons (<code>dict</code>)</p><p></p><p><code>buttons</code> properties:<br>label: any <code>string</code><br>color: <code>default</code> | <code>danger</code><br>onClick: <code>func</code><br><br>This value is required.</p></td></tr><tr><td>rows</td><td><p>a <code>list</code> of rows (<code>dict</code>)</p><p></p><p>row structure (<code>dict</code>):</p><p><code>id</code>: <strong>Must be unique</strong>. Duplicate IDs can cause unexpected behavior and may result in incorrect UI rendering. Ensure each ID is distinct to avoid issues.</p><p><code>cells</code>: a <code>list</code> of cells that match the structure of <code>columns</code><br></p><p>cell structure (<code>dict</code>):</p><p>tag: <code>text</code>, <code>color</code></p><p>text: <code>text</code>, <code>subtext</code>, <code>imageUrl</code></p><p>button: <code>{}</code></p><p><br>Default: <code>[]</code></p></td></tr><tr><td>search</td><td><code>True | False</code><br><br>Default: <code>True</code></td></tr><tr><td>items_per_page</td><td>any <code>int</code><br><br>Default: <code>50</code></td></tr><tr><td>selectable</td><td><code>True | False</code><br><br>Default: <code>False</code></td></tr><tr><td>visible</td><td>See <a href="/ui-scripting/api-reference#visible">Visible</a>.<br><br>Default: <code>True</code></td></tr></tbody></table>

### Events

<table><thead><tr><th>Event</th><th width="286">Arguments</th><th>Description</th></tr></thead><tbody><tr><td>onSelectionChange</td><td>selected_rows (<code>list</code> of row ids)</td><td>Triggered when the selection changes.</td></tr></tbody></table>


