---
url: /get-started/how-nocobase-works.md
---
# How NocoBase Works
---
url: /tutorials/v2/index.md
---
# NocoBase 2.0 Beginner Tutorial
This tutorial walks you through building a **minimal IT HelpDesk system** from scratch using NocoBase 2.0. The entire system requires only **2 data tables** and zero code, yet delivers ticket submission, category management, change tracking, access control, and a data dashboard.
## About This Tutorial
- **Target audience**: Business users, technical users, or anyone interested in NocoBase (some computer background is recommended)
- **Case project**: A minimal IT HelpDesk system with only 2 tables
- **Estimated time**: 2-3 hours (non-technical), 1-1.5 hours (technical users)
- **Prerequisites**: Docker environment or [Online Demo](https://demo.nocobase.com/new) (24-hour trial, no installation needed)
- **Version**: NocoBase 2.0
## What You'll Learn
Through 7 hands-on chapters, you'll master the core concepts and workflow of NocoBase:
| # | Chapter | Key Topics |
|---|---------|------------|
| 1 | [Getting Started — Up and Running in 5 Minutes](./01-getting-started/) | Docker install, UI Editor vs Usage mode |
| 2 | [Data Modeling — Building the Skeleton](./02-data-modeling/) | Collections, Fields, Relations |
| 3 | [Building Pages — Making Data Visible](./03-building-pages/) | Blocks, Table block, Filtering & Sorting |
| 4 | [Forms & Details — Entering Data](./04-forms-and-details/) | Form blocks, Field linkage |
| 5 | [Users & Permissions — Who Sees What](./05-roles-and-permissions/) | Roles, Menu permissions, Data permissions |
| 6 | [Workflows — Automation](./06-workflows/) | Notifications, Triggers |
| 7 | [Dashboard — The Big Picture](./07-dashboard/) | Charts, Markdown blocks |
## Data Model Preview
This tutorial is built around a minimal data model — just **2 tables**, but enough to cover data modeling, page building, form design, access control, workflows, and dashboards.
| Table | Key Fields |
|-------|-----------|
| Tickets | Title, Description, Status, Priority |
| Categories | Name, Color |
## FAQ
### What is NocoBase best suited for?
Internal business tools, data management systems, approval workflows, CRM, ERP, and other scenarios requiring flexible customization with self-hosted deployment.
### What prerequisites do I need?
No programming required, but some basic computer knowledge is recommended. The tutorial explains concepts like tables, fields, and relationships step by step. Experience with databases or spreadsheets is a plus.
### Can the tutorial system be extended?
Yes. This tutorial uses only 2 tables, but NocoBase supports complex multi-table relationships, external API integrations, and custom plugins.
### What deployment environment is needed?
Docker is recommended (Docker Desktop or Linux server), minimum 2 cores and 4GB RAM. Git source installation is also supported. For learning purposes, you can also request an [Online Demo](https://demo.nocobase.com/new) — no installation needed, valid for 24 hours.
### Are there limitations in the free version?
Core features are fully free and open-source. The commercial edition offers additional premium plugins and technical support. See [commercial pricing](https://www.nocobase.com/en/commercial) for details.
## Tech Stack
NocoBase 2.0 is built on:
- **Frontend**: React + [Ant Design](https://ant.design/) 5.0
- **Backend**: Node.js + Koa
- **Database**: PostgreSQL (also supports [MySQL](/get-started/installation/docker), MariaDB)
- **Deployment**: [Docker](/get-started/installation/docker), Kubernetes
## Platform Comparison
If you're evaluating no-code/low-code platforms:
| Platform | Highlights | Difference from NocoBase |
|----------|-----------|--------------------------|
| [Appsmith](https://www.appsmith.com/) | Open-source, strong frontend customization | NocoBase is more data-model driven |
| [Retool](https://retool.com/) | Internal tool platform | NocoBase is fully open-source, no usage limits |
| [Airtable](https://airtable.com/) | Online collaborative database | NocoBase supports self-hosted deployment |
| [Budibase](https://budibase.com/) | Open-source low-code, self-hostable | NocoBase has stronger plugin architecture |
## Related Docs
### Getting Started
- [How NocoBase Works](/get-started/how-nocobase-works) — Core concepts overview
- [Quick Start](/get-started/quickstart) — Installation and initial setup
- [System Requirements](/get-started/system-requirements) — Hardware and software requirements
### More Tutorials
- [NocoBase 1.x Tutorials](/tutorials/v1/) — Task management system tutorial with advanced topics
### Solutions
- [Ticket System Solution](/solution/ticket-system/) — AI-powered intelligent ticket management
- [CRM Solution](/solution/crm/) — Customer relationship management
- [AI Employees](/ai-employees/quick-start) — Add AI capabilities to your system
Ready? Let's start with [Chapter 1: Getting Started](./01-getting-started/)!
---
url: /tutorials/v2/01-getting-started.md
---
# Chapter 1: Getting Started — Build a Working System in 5 Minutes
In this series, we'll build a **minimal IT HelpDesk system** from scratch using NocoBase. The entire system needs only **2 [data tables](/data-sources/main/collection)** and zero lines of code — yet it will support ticket submission, category management, change tracking, access control, and even a [dashboard](/data-visualization).
This chapter walks you through deploying NocoBase with [Docker](/get-started/installation/docker), completing your first login, and understanding the difference between [UI Editor mode and Usage mode](/get-started/how-nocobase-works).
## 1.1 What Is NocoBase
Have you ever been in one of these situations?
- Your team needs an internal system, but off-the-shelf software never quite fits
- Hiring developers for a custom build is too expensive and too slow, and requirements keep changing
- You're using spreadsheets as a workaround, but the data keeps getting messier
**NocoBase was built to solve this problem.** It's an open-source, highly extensible **AI-powered no-code development platform**. You can build your own business systems through configuration and drag-and-drop — no coding required.
Compared to other no-code tools, NocoBase has a few core principles:
- **Data model driven**: Define your data structure first, then use [blocks](/interface-builder/blocks) to display data, then [actions](/interface-builder/actions) to process it — UI and data are fully decoupled
- **WYSIWYG**: [Pages](/interface-builder/pages) are your canvas. Click anywhere to edit, as intuitive as building a Notion page
- **Everything is a plugin**: All features are [plugins](/development/plugin), similar to WordPress — install what you need
- **AI built into your workflow**: Built-in [AI employees](/ai-employees/quick-start) that can perform analysis, translation, data entry, and more
- **Open source + self-hosted**: Core code is fully open source, all data stays on your own server
## 1.2 Installing NocoBase
NocoBase supports multiple installation methods. We'll go with the simplest: **Docker**.
### Prerequisites
You need [Docker](https://docs.docker.com/get-docker/) and Docker Compose installed on your machine, with the Docker service running. Windows, Mac, and Linux are all supported.
### Step 1: Download the Configuration File
Open your terminal (PowerShell on Windows, Terminal on Mac) and run:
```bash
# Create a project directory and enter it
mkdir my-project && cd my-project
# Download docker-compose.yml (defaults to PostgreSQL)
curl -fsSL https://static-docs.nocobase.com/docker-compose/en/latest-postgres.yml -o docker-compose.yml
```
> **Other databases?** Replace `postgres` in the URL with `mysql` or `mariadb`.
> You can also choose different versions: `latest` (stable), `beta` (testing), or `alpha` (development). See the [official installation docs](https://docs.nocobase.com/get-started/installation/docker) for details.
>
> | Database | Download URL |
> |----------|-------------|
> | PostgreSQL (recommended) | `https://static-docs.nocobase.com/docker-compose/en/latest-postgres.yml` |
> | MySQL | `https://static-docs.nocobase.com/docker-compose/en/latest-mysql.yml` |
> | MariaDB | `https://static-docs.nocobase.com/docker-compose/en/latest-mariadb.yml` |
### Step 2: Start It Up
```bash
# Pull images
docker compose pull
# Start in background (first run will auto-install)
docker compose up -d
# Watch logs to confirm successful startup
docker compose logs -f app
```
When you see this line in the output, you're good to go:
```
🚀 NocoBase server running at: http://localhost:13000/
```

### Step 3: Log In
Open your browser and go to `http://localhost:13000`. Log in with the default credentials:
- **Email**: `admin@nocobase.com`
- **Password**: `admin123`
> Remember to change the default password after your first login.
## 1.3 Getting to Know the Interface
After logging in, you'll see a clean initial interface. Don't worry about it being empty — let's first understand two key concepts.
### UI Editor Mode vs Usage Mode
NocoBase has two interface modes:
| Mode | Description | Who Uses It |
|------|-------------|-------------|
| **Usage Mode** | The everyday interface for regular users | Everyone |
| **UI Editor Mode** | The design mode for building and tweaking the interface | Admins |
To switch: click the **"UI Editor"** button in the top-right corner (a highlighter pen icon).

When you enable UI Editor mode, you'll notice **orange highlight borders** appearing around many elements on the page — this means they're configurable. Each configurable element shows a small icon in its top-right corner; click it to access its settings.
Here's what it looks like on a demo system:

As shown above: [menus](/interface-builder/menus), table action bars, and the bottom of the page all show orange indicators. Click them to create or configure elements.
> **Remember this pattern**: In NocoBase, whenever you want to modify something on the page, enter UI Editor mode, find the small icon in its top-right corner, and click it.
### Basic Interface Layout
The NocoBase interface is composed of three areas:
```
┌──────────────────────────────────────────┐
│ Top Navigation Bar │
├──────────┬───────────────────────────────┤
│ │ │
│ Left │ Content Area │
│ Sidebar │ (place blocks here) │
│ (groups) │ │
│ │ │
└──────────┴───────────────────────────────┘
```
- **Top Navigation Bar**: Houses top-level menus for switching between modules
- **Left Sidebar (groups)**: If using group menus, this shows second-level navigation to organize page hierarchy
- **Content Area**: The main body of the page, where you place various **Blocks** to display and interact with data

It's still empty for now — but starting from the next chapter, we'll fill it up.
## 1.4 What We're Going to Build
Over the course of this tutorial, we'll build an **IT HelpDesk system** step by step. It will support:
- ✅ Ticket submission: Users fill in title, description, category, and priority
- ✅ Ticket list: Filter by status or category at a glance
- ✅ Access control: Regular [users](/users-permissions/user) see only their own tickets; admins see everything
- ✅ Dashboard: Real-time statistics on ticket distribution and trends
- ✅ Audit log (built-in)
The entire system needs just **2 data tables**:
| Table | Purpose | Custom Fields |
|-------|---------|---------------|
| Categories | Ticket categories (e.g., Network Issue, Software Bug) | 2 |
| Tickets | The core table — each row is one ticket | 7-8 |
That's right, just 2 tables. Common capabilities like users, [permissions](/users-permissions/role), file management, departments, email, and audit logs are all provided by built-in NocoBase plugins — no need to reinvent the wheel. We only need to focus on our business data.
## Summary
In this chapter we:
1. Learned what NocoBase is — an open-source no-code platform
2. Installed and started NocoBase with Docker in one step
3. Understood the two interface modes (UI Editor / Usage) and the basic layout
4. Previewed the HelpDesk system we're going to build
**Next chapter**: We'll get hands-on — enter the [Data Source Manager](/data-sources) and create our first data table. This is the skeleton of the entire system and NocoBase's most fundamental capability.
See you in Chapter 2!
## Related Resources
- [Docker Installation Guide](/get-started/installation/docker) — Full installation options and environment variables
- [System Requirements](/get-started/system-requirements) — Hardware and software requirements
- [How NocoBase Works](/get-started/how-nocobase-works) — Core concepts: data sources, blocks, actions
---
url: /tutorials/v2/02-data-modeling.md
---
# Chapter 2: Data Modeling — Two Tables for a Complete Ticket System
In the last chapter, we installed NocoBase and got familiar with the interface. Now it's time to build the skeleton of our HelpDesk system — the **data model**.
This chapter creates two [collections](/data-sources/main/collection) — Tickets and Categories — and configures [field types](/data-sources/field) ([single-line text](/data-sources/field/basic/input), [dropdown](/data-sources/field/choices/select), [many-to-one](/data-sources/field/associations/m2o) relations). The data model is the foundation: figure out what data you need and how it's related, then building pages and setting permissions becomes straightforward.
## 2.1 What Are Collections and Fields
If you've used Excel before, this will feel familiar:
| Excel Concept | NocoBase Concept | Description |
|---------------|-----------------|-------------|
| Worksheet | Collection | A container for one type of data |
| Column header | Field | An attribute describing the data |
| Each row | Record | One specific piece of data |

For example, our "Tickets" collection is like an Excel spreadsheet — each column is a field (Title, Status, Priority...), and each row is one ticket record.
But NocoBase is much more powerful than Excel. It supports multiple **collection types**, each with different built-in capabilities:
| Type | Best For | Examples |
|------|----------|----------|
| **General** | Most business data | Tickets, Orders, Customers |
| **Tree** | Hierarchical data | Category trees, Org charts |
| Calendar | Date-based events | Meetings, Schedules |
| File | Attachment management | Documents, Images |
Today we'll use **General** and **Tree** collections. We'll cover the others when needed.
**Enter Data Source Manager**: Click the **"Data Source Manager"** icon in the bottom-left corner (the database icon next to the gear). You'll see the "Main [data source](/data-sources)" — this is where all our tables live.

## 2.2 Creating the Core Table: Tickets
Let's jump right in and create the heart of our system — the Tickets table.
### Create the Table
1. On the Data Source Manager page, click **"Main data source"** to enter

2. Click **"Create collection"**, then select **"General collection"**

3. Collection name: `tickets`, Display name: `Tickets`

When creating a table, the system checks a set of **system fields** by default. These automatically track metadata for every record:
| Field | Description |
|-------|-------------|
| ID | Primary key, unique identifier |
| Created at | When the record was created |
| Created by | Who created the record |
| Last updated at | When it was last modified |
| Last updated by | Who last modified it |
Keep these defaults as-is — no manual management needed. You can uncheck them if a specific scenario doesn't need them.
### Adding Basic Fields
The table is created. Now let's add fields. Click **"Configure fields"** on the Tickets table, and you'll see the default system fields already listed.


Click the **"Add field"** button in the top-right corner to expand a dropdown of field types — pick the one you want to add.

We'll add the ticket's own fields first; relation fields come later.
**1. Title (Single line text)**
Every ticket needs a short title to summarize the issue. Click **"Add field"** → select **"Single line text"**:

- Field name: `title`, Display name: `Title`
- Click **"Set validation rules"**, add a **"Required"** rule

**2. Description (Markdown(Vditor))**
For detailed problem descriptions with rich formatting — images, code blocks, etc. Under **"Add field"** → **"Media"** category, you'll find three options:
| Field Type | Features |
|-----------|----------|
| Markdown | Basic Markdown, simple styling |
| Rich Text | Rich text editor with attachment uploads |
| **Markdown(Vditor)** | Most feature-rich: WYSIWYG, instant rendering, and source code editing modes |
We'll go with **Markdown(Vditor)**.

- Field name: `description`, Display name: `Description`

**3. Status (Single select)**

Tickets go through stages from submission to completion, so we need a status field to track progress.
- Field name: `status`, Display name: `Status`
- Add option values (each option needs a "Value" and "Label"; color is optional):
| Value | Label | Color |
|-------|-------|-------|
| pending | Pending | Orange |
| in_progress | In Progress | Blue |
| completed | Completed | Green |

Fill in the options and save first. Then click **"Edit"** on this field again — now you can set the "Default value" to **"Pending"**.


> The first time you create the field, there are no options yet, so you can't pick a default value — you need to save first, then come back to set it.
> Why a single select? Because status is a fixed set of values. A dropdown prevents users from entering arbitrary text, keeping data clean.
**4. Priority (Single select)**
Helps distinguish urgency so the team can sort and tackle tickets efficiently.
- Field name: `priority`, Display name: `Priority`
- Add option values:
| Value | Label | Color |
|-------|-------|-------|
| low | Low | |
| medium | Medium | |
| high | High | Orange |
| urgent | Urgent | Red |
At this point, the Tickets table has 4 basic fields. But — shouldn't a ticket have a "category"? Like "Network Issue" or "Software Bug"?
We could make Category a dropdown, but you'd quickly run into a problem: categories can have sub-categories ("Hardware" → "Monitor", "Keyboard", "Printer"), and dropdowns can't handle that.
We need **a separate table** for categories. And NocoBase's **Tree collection** is perfect for this.
## 2.3 Creating the Categories Tree Table
### What Is a Tree Collection
A tree collection is a special type of table with built-in **parent-child relationships** — every record can have a parent node. This is ideal for hierarchical data:
```
Hardware ← Level 1
├── Monitor ← Level 2
├── Keyboard & Mouse
└── Printer
Software
├── Office Apps
└── System Issues
Network
Account
```
With a general collection, you'd have to manually create a "Parent Category" field to build this hierarchy. A **tree collection handles it automatically** and supports tree views, adding child records, and more.
### Create the Table
1. Go back to Data Source Manager, click **"Create collection"**
2. This time, select **"Tree collection"** (not General!)

3. Collection name: `categories`, Display name: `Categories`

> After creation, you'll notice the table has two extra relation fields — **"Parent"** and **"Children"** — beyond the standard system fields. This is the tree collection's special power. Use Parent to access the parent node and Children to access all child nodes, without any manual setup.

### Add Fields
Click **"Configure fields"** to enter the field list. You'll see the system fields plus the auto-generated Parent and Children fields.
Click **"Add field"** in the top-right:
**Field 1: Category Name**
1. Select **"Single line text"**
2. Field name: `name`, Display name: `Name`
3. Click **"Set validation rules"**, add a **"Required"** rule
**Field 2: Color**
1. Select **"Color"**
2. Field name: `color`, Display name: `Color`

The Color field gives each category its own visual identity — it will make the interface much more intuitive later.

With that, both tables' basic fields are configured. Now let's link them together.
## 2.4 Back to Tickets: Adding Relation Fields
> **Relation fields can be a bit abstract at first.** If it doesn't click right away, feel free to skip ahead to [Chapter 3: Building Pages](./03-building-pages/) and see how data is displayed in practice, then come back here to add the relation fields.
Tickets need to be linked to a category, a submitter, and an assignee. These are called **relation fields** — instead of storing text directly (like "Title" does), they store the ID of a record in another table, and use that ID to look up the corresponding record.
Let's look at a specific ticket — on the left are the ticket's attributes. "Category" and "Submitter" don't store text; they store an ID. The system uses that ID to find the exact matching record from the tables on the right:

On the interface, you see names like "Network" and "Alice", but behind the scenes it's all connected by IDs. **Multiple tickets can point to the same category or the same user** — this relationship is called **[Many-to-one](/data-sources/field/associations/m2o)**.
### Adding Relation Fields
Go back to Tickets → "Configure fields" → "Add field", select **"Many to one"**.

You'll see these configuration options:
| Option | Description | How to Fill |
|--------|-------------|-------------|
| Source collection | Current table (auto-filled) | Don't change |
| **Target collection** | Which table to link to | Select the target |
| **Foreign key** | The linking column stored in the current table | Enter a meaningful name |
| Target collection key field | Defaults to `id` | Keep as-is |
| ON DELETE | What happens when the target record is deleted | Keep as-is |

> The foreign key defaults to a random name like `f_xxxxx`. We recommend changing it to something meaningful for easier maintenance. Use lowercase with underscores (e.g., `category_id`) instead of camelCase.
Add the following three fields:
**5. Category → Categories table**
- Display name: `Category`
- Target collection: Select **"Categories"** (if not in the list, type the name and it will be auto-created)
- Foreign key: `category_id`
**6. Submitter → Users table**
Records who submitted this ticket. NocoBase has a built-in Users table — just link to it.
- Display name: `Submitter`
- Target collection: Select **"Users"**
- Foreign key: `submitter_id`

**7. Assignee → Users table**
Records who is responsible for handling this ticket.
- Display name: `Assignee`
- Target collection: Select **"Users"**
- Foreign key: `assignee_id`

## 2.5 The Complete Data Model
Let's review the full data model we've built:

`}o--||` represents a many-to-one relationship: "many" on the left, "one" on the right.
## Summary
In this chapter we completed the data modeling — the entire skeleton of our HelpDesk system:
1. **Tickets** (`tickets`): 4 basic fields + 3 relation fields, created as a **General collection**
2. **Categories** (`categories`): 2 custom fields + auto-generated Parent/Children fields, created as a **Tree collection** with built-in hierarchy support
Key concepts we learned:
- **Collection** = A container for one type of data
- **Collection types** = Different types for different scenarios (General, Tree, etc.)
- **Field** = A data attribute, created via "Configure fields" → "Add field"
- **System fields** = ID, Created at, Created by, etc. — auto-checked when creating a table
- **Relation field (Many-to-one)** = Points to a record in another table, linking tables together
> You may notice that later screenshots already contain data — we pre-loaded test data for demonstration purposes. In NocoBase, all CRUD operations are done through the frontend pages. Chapter 3 covers building tables to display data, and Chapter 4 covers forms for data entry — stay tuned.
## Next Chapter Preview
The skeleton is ready, but the tables are still empty. In the next chapter, we'll build pages to make the data visible.
See you in Chapter 3!
## Related Resources
- [Data Sources Overview](/data-sources) — Core data modeling concepts in NocoBase
- [Field Types](/data-sources/field) — Complete field type reference
- [Many-to-One Relations](/data-sources/field/associations/m2o) — Relationship configuration guide
---
url: /tutorials/v2/03-building-pages.md
---
# Chapter 3: Building Pages — From Blank to Functional
In the last chapter, we built the skeleton of our data tables — but right now the data only lives in the "backend." Users can't see it at all. In this chapter, we'll bring our data **front and center**: create a [Table block](/interface-builder/blocks/data-blocks/table) to display ticket data, configure field visibility, sorting, [filtering](/interface-builder/blocks/filter-blocks/form), and pagination, turning it into a real, usable ticket list.
## 3.1 What Is a Block
In NocoBase, a **Block** is a building brick on a page. Want to show a table? Drop in a Table block. Need a form? Add a Form block. A single page can freely combine multiple blocks, and you can drag and drop to rearrange the layout.
Common block types:
| Type | Purpose |
|------|---------|
| Table | Displays multiple records in rows and columns |
| Form | Lets users input or edit data |
| Details | Shows the full information of a single record |
| Filter Form | Provides filter criteria to narrow down data in other blocks |
| Chart | Pie charts, line charts, and other visualizations |
| Markdown | A section of custom text or instructions |
Remember this analogy: **Blocks = building bricks**. We're about to use them to assemble our tickets page.
## 3.2 Adding a Menu and Pages
First, we need to create an entry point for "Tickets" in the system.
1. Click the **[UI Editor](/get-started/how-nocobase-works)** toggle in the top-right corner to enter design mode (the entire page will show orange editable borders).
2. Click the **"Add menu item"** button (`+` icon) in the top navigation bar, select **"Add group"**, and name it **"Tickets"**.
3. The "Tickets" [menu](/interface-builder/menus) appears immediately in the top navigation bar. **Click on it** — a sidebar menu will expand on the left.
4. In the sidebar, click the orange **"Add menu item"** button, select **"Modern page (v2)"**, and add two sub-[pages](/interface-builder/pages) one by one:
- **All Tickets** — displays all tickets
- **Categories** — manages category data


> **Note**: You'll see both "Classic page (v1)" and "Modern page (v2)" options. This tutorial uses **v2** throughout.
## 3.3 Adding a Table Block
Now go to the "All Tickets" page and add a Table block:
1. On the blank page, click **"Add block"**.
2. Select **Data blocks -> Table**.
3. In the [collection](/data-sources/main/collection) list that pops up, select **"Tickets"** (the table we created in the last chapter).

Once the Table block is added, you'll see an empty table on the page.

An empty table with no data isn't very useful for testing. Let's quickly add an "Add new" button so we can enter some test data:
1. Click **"Configure actions"** in the top-right corner of the table, and check **"Add new"**.

2. Click the new **"Add new"** button, then in the popup select **Add block → Form (Add New) → Current collection**.

3. In the popup, click **"Configure fields"** and check the fields you need (Title, Status, Priority, etc.); click **"Configure actions"** and enable the **"Submit"** button.


4. Fill in a few test tickets and submit — you'll see the data appear in the table.

> We'll cover form configuration in detail (field linkage, edit forms, detail popups, etc.) in [Chapter 4](/tutorials/v2/04-forms-and-details/). For now, just being able to enter data is enough.
## 3.4 Configuring Display Columns
By default, a table won't automatically show all [fields](/data-sources/field). We need to manually choose which columns to display:
1. On the right side of the Table block header, click **"Fields"**.
2. Check the fields you want to show:
- **Title** — the ticket subject, visible at a glance
- **Status** — current processing progress
- **Priority** — urgency level
- **Category** — a relation field that will display the category name
- **Submitter** — who submitted the ticket
- **Assignee** — who is responsible
3. Fields you don't need to display (like ID or Created at) can be left unchecked to keep the table clean.

> **Tip**: You can drag and drop to reorder the displayed fields. Put the most important ones — "Title" and "Status" — up front so key information is visible at a glance.
### Relation Fields Showing IDs
After enabling "Category," you'll notice the table shows category IDs (numbers) instead of names. That's because relation fields default to using ID as the title field. Two ways to fix this:
**Option A: Change it in the column settings (current table only)**
Click the column settings for the "Category" column and find **"Title field"**. Change it from ID to **Name**. This only affects the current Table block.


**Option B: Change it in the data source (global, recommended)**
Go to **Settings -> [Data sources](/data-sources) -> Collections -> Categories**, and change the **"Title field"** to **Name**. All blocks referencing the Categories collection will then display names by default. After the change, you'll need to re-add the field on the page for it to take effect.

## 3.5 Adding Filtering and Sorting
As tickets pile up, we'll need to quickly find specific ones. NocoBase provides several ways to filter data — let's start with the most common: the **Filter Form block**.
### Adding a Filter Form
1. On the All Tickets page, click **"Add block"** and select **Filter blocks -> Filter form**.
2. In v2 pages, there's no collection selection step — the Filter form is added directly to the page.
3. In the Filter form, click **"Fields"**. A list of all filterable data blocks on the current page will appear, e.g., `Table: Tickets #c48b` (the code after `#` is the block's UID, useful for distinguishing multiple blocks from the same collection).

4. Hover over a block name to expand its list of filterable fields. Click to add them: **Status**, **Priority**, **Category**.

5. Once added, users can type filter criteria and the table data will **update automatically in real time**.

### Multi-Field Fuzzy Search
What if you want a single search box to match across multiple fields at once?
Click the settings icon in the top-right corner of a search field and you'll see **"Connect fields"**. It lists all searchable fields from each block on the page — by default, only "Title" is connected.

Select additional fields like **Description** so that a keyword search matches all of them simultaneously.
You can even search through relation fields — click "Category," then in the next level check "Category Name." Now searches will also match against category names.


> **Connect fields is powerful**: it works across multiple blocks and multiple fields. If your page has several data blocks, try adding more and experiment!
### Don't Want Auto-Filtering?
If you'd prefer users to click a button before filtering takes effect, click **"[Actions](/interface-builder/actions)"** at the bottom-right of the Filter form and enable the **"Filter"** and **"Reset"** buttons. Users will then need to click "Filter" to apply their criteria.

### Alternative: The Table's Built-in Filter Action
Besides a dedicated Filter Form block, the Table block itself has a built-in **"Filter"** action. Click **"Actions"** above the Table block and enable **"Filter"**. A filter button will appear in the table toolbar. Clicking it opens a condition panel where users can filter data by field values directly.


If you don't want users to hunt for fields every time they open the filter, you can pre-configure default filter fields in the Filter button's settings — so the most common criteria are ready to use right away.

> **Note**: The table's built-in Filter action currently **does not support fuzzy search** — it only handles exact matches and condition-based filtering. If you need fuzzy search, use the Filter Form block with "Connect fields" described above.
### Setting Default Sorting
We want the newest tickets to appear at the top:
1. Click the **block settings** icon (three-line icon) in the top-right corner of the Table block.
2. Find **"Set default sorting rules"**.
3. Add a sort field: select **Created at**, set the order to **Descending**.

This way, newly submitted tickets always appear at the top, making them easier to handle.
## 3.6 Configuring Row Actions
Viewing a list isn't enough — we also need to click into tickets to see details and make edits.
1. In the actions column, click the second "+" icon.
2. Click to add actions: **View**, **[Edit](/interface-builder/actions/edit)**, **[Delete](/interface-builder/actions/delete)**.
3. Each row will now have "View", "Edit", and "Delete" buttons in the actions column.

Clicking the "View" or "Edit" button opens a Drawer where we can place blocks to show or edit the full record. We'll configure that in detail in the next chapter. Clicking "Delete" removes the row.
## 3.7 Adjusting Page Layout
By now the page has both a Filter Form and a Table block, but they're stacked vertically by default — which may not look great. NocoBase lets you **drag and drop** to rearrange blocks.
In design mode, hover over the drag handle at the top-left corner of a block (the cursor will change to a crosshair), then hold and drag.
**Drag the Filter Form above the Table**: Grab the Filter Form block and move it toward the top edge of the Table block. When a blue guide line appears, release — the Filter Form will snap into place above the Table.
**Drag filter fields onto the same row**: Inside the Filter Form, fields are stacked vertically by default. Drag "Priority" to the right of "Status" — when a vertical guide line appears, release. The two fields will sit side by side on one row, saving vertical space.
> Almost everything in NocoBase supports drag-and-drop — action buttons, table columns, menu items, and more. Feel free to explore!
## 3.8 Configuring the Categories Page
Don't forget — we created a "Categories" sub-page back in section 3.2. Now let's add content to it. The setup is similar to the ticket list — add a Table block, check fields, configure actions — so we won't repeat every step. Just one key difference.
Remember the "Categories" collection we created in Chapter 2? It's a **tree table** (supports parent-child hierarchy). To display the tree structure correctly, you need to enable a setting:
1. Go to the "Categories" page and add a Table block, selecting the "Categories" collection.
2. Click the Table block's **block settings** (three-line icon), find **"Tree table"**, and toggle it on.
Once enabled, the table will display categories in an indented hierarchy showing parent-child relationships, instead of listing all records flat.
3. Check the fields you want to display (e.g., Name, Description), and configure row actions ([Add new](/interface-builder/actions/add-new), Edit, Delete).
4. **Layout tip**: Put "Name" in the first column and "Actions" in the second. The categories table doesn't have many fields, so a two-column layout is more compact and user-friendly.
[Screenshot: Categories tree table configured]
## Summary
Congratulations! Our ticket system now has a proper **management interface**:
- A clear menu structure (Tickets -> All Tickets / Categories)
- A **Table block** displaying ticket data
- A **Filter Form** for quick filtering by status, priority, and category
- **Sorting rules** that order tickets by creation time, newest first
- Row action buttons for convenient viewing and editing
- A **tree table** displaying category hierarchy
Easier than you expected, right? The entire process required zero lines of code — everything was done through drag-and-drop and configuration.
## Next Chapter Preview
Being able to "see" data isn't enough — users also need to **submit new tickets**. In the next chapter, we'll build Form blocks, configure field linkage rules, and enable change history to track every modification to a ticket.
## Related Resources
- [Blocks Overview](/interface-builder/blocks) — All block types explained
- [Table Block](/interface-builder/blocks/data-blocks/table) — Table block configuration guide
- [Filter Block](/interface-builder/blocks/filter-blocks/form) — Filter form setup
---
url: /tutorials/v2/04-forms-and-details.md
---
# Chapter 4: Forms & Details — Input, Display, All in One
In the last chapter, we built the ticket list and used a quick form to enter test data. In this chapter, we'll **refine the form experience** — optimize [Form block](/interface-builder/blocks/data-blocks/form) field layouts, add [Details blocks](/interface-builder/blocks/data-blocks/details), configure [linkage rules](/interface-builder/linkage-rules), and use [change history](https://docs.nocobase.com/record-history/) to track every modification.
:::tip
Section 4.4 "Change History" requires the [Professional edition](https://www.nocobase.com/en/commercial). Skipping it won't affect later chapters.
:::
## 4.1 Refining the New Ticket Form
In the last chapter, we quickly created a working "Add new" form. Now let's refine it — adjust field order, set default values, and optimize the layout. If you skipped the quick form in the previous chapter, no worries — we'll walk through creating one from scratch here.
### Adding the "Add new" Action Button
1. Make sure you're in [UI Editor](/get-started/how-nocobase-works) mode (top-right toggle is on).
2. Go to the "All Tickets" page, and click **"[Actions](/interface-builder/actions)"** above the Table block.
3. Check the **"Add new"** action button.
4. An "Add new" button will appear above the table. Clicking it opens a [popup](/interface-builder/actions/pop-up).

### Configuring the Form in the Popup
1. Click the "Add new" button to open the popup.
2. In the popup, click **"Add block" -> Data blocks -> [Form (Add new)](/interface-builder/blocks/data-blocks/form)**.
3. Select **"Current collection"**. The popup already knows which collection it's associated with — no need to specify manually.

4. In the form, click **"[Fields](/data-sources/field)"** and check the following fields:
| Field | Configuration Notes |
|-------|-------------------|
| Title | Required (auto-inherited) |
| Description | Rich text input |
| Status | Dropdown select (we'll set a default via linkage rules later) |
| Priority | Dropdown select |
| Category | A relation field that automatically appears as a dropdown selector |
| Submitter | Relation field (we'll set a default via linkage rules later) |
| Assignee | Relation field |

You'll notice a red asterisk `*` next to "Title" automatically — because we set it as required when creating the field in Chapter 2. Forms automatically inherit required rules from the [collection](/data-sources/main/collection)'s field settings; no extra configuration needed.

> **Tip**: If a field isn't required at the collection level but you want it required in this specific form, you can set it individually in the field's settings.

### Adding the Submit Button
1. Below the Form block, click **"Actions"**.
2. Check the **"Submit"** button.

3. After filling in the form, users can click Submit to create a new ticket.

## 4.2 Linkage Rules: Defaults & Field Linkage
Some fields should be auto-filled (e.g., Status defaults to "Pending"), while others need to change dynamically based on conditions (e.g., urgent tickets require a description). The default value feature in 2.0 is still evolving, so in this tutorial we'll use **Linkage Rules** for both default values and field linkage.
1. Click the **block settings** icon (three-line icon) in the top-right corner of the Form block.
2. Find **"Linkage rules"** — clicking it opens a configuration panel in the sidebar.

### Setting Default Values
Let's first set default values for "Status" and "Submitter":
1. Click **"Add linkage rule"**.
2. **Leave the condition empty** — an unconditional linkage rule executes immediately when the form loads.

3. Configure the Actions:
- Status field -> **Set default value** -> Pending
- Submitter field -> **Set default value** -> Current user
> **Important notes on setting values**: Always select **"Current form"** as the data source first. For relation fields (like Category, Submitter, Assignee — any many-to-one field), you must select the object property itself, not its expanded sub-fields.
>
> When selecting variables (like "Current user"), first **single-click** to select it, then **double-click** to fill it into the selection bar.



If you want a field to be non-editable by the submitter (e.g., Status), you can set its **"Display mode"** to **"Readonly"** in the field settings.

> **Three display modes**: Editable, Readonly (editing disabled but field appearance preserved), and Easy-reading (displays as plain text only).

### Urgent Ticket Requires Description
Next, add a conditional linkage rule: when a user selects Priority as "Urgent", the Description field becomes **required**, reminding the submitter to clearly describe the situation.
1. Click **"Add linkage rule"**.

2. Configure the rule:
- **Condition**: Current form / Priority **equals** Urgent
- **Actions**: Description field -> set to **Required**


3. Save the rule.
Now test it: select Priority as "Urgent" and a red asterisk `*` will appear next to the Description field, indicating it's required. Select any other priority and it reverts to optional.

Finally, apply what we've learned and adjust the layout a bit.

> **What else can linkage rules do?** Beyond setting defaults and controlling required status, they can also show/hide fields and dynamically assign values. For example: when Status is "Closed", hide the Assignee field. We'll explore more in later chapters as we encounter these scenarios.
## 4.3 Details Block
In the last chapter, we added a "View" button to table rows that opens a Drawer. Now let's configure what goes inside the Drawer.
1. In the table, click the **"View"** button on any row to open the Drawer.
2. In the Drawer, click **"Add block" -> Data blocks -> [Details](/interface-builder/blocks/data-blocks/details)**.
3. Select **"Current collection"**.

4. In the Details block, click **"Fields"** with this layout:
| Area | Fields |
|------|--------|
| Top | Title, Status (tag style) |
| Main body | Description (rich text area) |
| Side info | Category name, Priority, Submitter, Assignee, Created at |
How to add a large title? Select Fields > Markdown > Edit Markdown > in the editing area, select a variable > Current record > Title. This dynamically inserts the record's title into a Markdown block. Delete the default text and use Markdown syntax to make it a heading (add `##` followed by a space before it).


Now the original Title field can be removed. Adjust the details layout:

> **Tip**: You can drag multiple fields onto the same row for a more compact and visually appealing layout.
5. In the Details block's **"Actions"**, check the **"Edit"** button so users can jump straight from the details view into edit mode.

### Configuring the Edit Form
Click the "Edit" button and a new popup opens — it needs an edit form inside. The edit form has nearly the same fields as the "Add new" form. Do we really have to check all those fields again from scratch?
Nope. Let's **save the "Add new" form as a template** first, then the edit form can reference it directly.
**Step 1: Go back to the "Add new" form and save as template**
1. Close the current drawer, go back to the ticket list, and click "Add new" to open the form.
2. Click the **block settings** icon (three-line icon) at the top-right of the Form block, and find **"Save as template"**.

3. Click save — it defaults to **"Reference"**. All forms referencing this template share the same configuration. Update one place and all stay in sync.


> Since our ticket form isn't complex, "Reference" is simpler to maintain. If you choose "Duplicate", each form gets an independent copy that can be modified separately.
**Step 2: Reference the template in the edit popup**
1. Go back to the details drawer or the table's row actions, and click the "Edit" button to open the edit popup.
You might think: just use **"Add block -> Other blocks -> Block templates"** to create the form, right? Try it and you'll find — this creates an **"Add new" form**, and the fields aren't actually populated. This is a common pitfall.

The correct approach:
2. In the popup, click **"Add block" -> Data blocks -> Form (Edit)** to create an edit form block first.
3. In the edit form, click **"Fields" -> "Field templates"**, and select the template you saved earlier.
4. All fields will be populated at once, matching the "Add new" form exactly.
5. Don't forget to add a **"Submit"** action button so users can save their changes.


Want to add a field later? Just modify the template once — both the "Add new" and edit forms update automatically.
### Quick Editing: Change Data Without Opening a Popup
Besides popup editing, NocoBase also supports **quick editing** directly in the table — no popups needed, just hover and click.
There are two places to enable it:
- **Table block level**: Click the Table block's **block settings** (three-line icon), find **"Quick editing"**, and toggle it on. This enables quick editing for all fields in the table.
- **Individual field level**: Click a column's field settings, find **"Quick editing"**, and toggle it on per field.

Once enabled, hovering over a table cell reveals a small pencil icon. Click it to open an inline editor for that field — changes save automatically.

> **When is this useful?** Quick editing is perfect for batch updates like changing status or assignee. For example, an admin browsing the ticket list can click the "Status" column to quickly change a ticket from "Pending" to "In Progress" without opening each one individually.
## 4.4 Enabling Change History
:::info Commercial Plugin
"[Record History](https://docs.nocobase.com/record-history/)" is included in the NocoBase [Professional edition](https://www.nocobase.com/en/commercial) and requires a commercial license. If you're using the Community edition, feel free to skip this section — it won't affect later chapters.
:::
One of the most important aspects of a ticket system: **who changed what and when must be traceable**. NocoBase's "Record History" plugin automatically logs every data change.
### Configuring Change History
1. Go to **Settings -> Plugin manager** and make sure the "Record History" plugin is enabled.

2. Enter the plugin configuration page and click **"Add collection"**, then select **"Tickets"**.
3. Choose which fields to track: **Title, Status, Priority, Assignee, Description**, etc.

> **Recommendation**: You don't need to track every field. Fields like ID and Created at that are never manually changed don't need to be tracked. Only record changes to fields that matter for the business.
4. Back in the settings, click **"Sync history data snapshot"**. The plugin will automatically create an initial history record for all existing tickets. Every subsequent change will add a new history entry.


### Viewing History in the Details Page
1. Go back to the ticket details Drawer (click the "View" button on a table row).
2. In the Drawer, click **"Add block" -> Record History**.
3. Select **"Current collection"** and choose **"Current record"** as the data source.
4. A timeline will appear at the bottom of the details page, clearly showing every change: who changed which field from what value to what value, and when.


This way, even if a ticket passes through multiple people, every change is crystal clear.
## Summary
In this chapter, we completed the full data lifecycle:
- **Form** — Users can submit new tickets with default values and validation
- **Linkage rules** — Urgent tickets automatically require a description
- **Details block** — Clearly displays a ticket's complete information
- **[Change history](/collection-templates/audit-log)** — Automatically tracks every modification for worry-free auditing (commercial plugin, optional)
From "visible" to "enterable" to "traceable" — our ticket system now has basic usability.
## Related Resources
- [Form Block](/interface-builder/blocks/data-blocks/form) — Form block configuration guide
- [Details Block](/interface-builder/blocks/data-blocks/details) — Details block setup
- [Linkage Rules](/interface-builder/linkage-rules) — Field linkage configuration
---
url: /tutorials/v2/05-roles-and-permissions.md
---
# Chapter 5: Users & Permissions — Who Sees What
In the last chapter, we built forms and detail [pages](/interface-builder/pages) — our ticket system can now handle data entry and viewing. But there's a problem: everyone sees the same thing after logging in. Regular employees who submit tickets can access the admin pages, technicians can delete categories... that's not going to work.
In this chapter, we'll add "access control": create [roles](/users-permissions/role), configure [menu permissions](/users-permissions/role/menu-permissions) and [data scope](/users-permissions/role/data-scope) restrictions — **different people see different [menus](/interface-builder/menus) and operate on different data**.
## 5.1 Understanding Roles
In NocoBase, **a [role](/users-permissions/role) is a collection of [permissions](/users-permissions/role)**. You don't need to configure permissions for each user individually — instead, you define a few roles first, then assign users to the appropriate roles.
NocoBase comes with three built-in roles after installation:
- **Root**: Super admin with full permissions — cannot be deleted
- **Admin**: Administrator with UI configuration permissions by default
- **Member**: Regular member with limited default permissions
But these three built-in roles aren't enough for our needs. Our ticket system requires finer-grained control, so we'll create 3 custom roles next.
## 5.2 Creating Three Roles
Open the settings menu in the top-right corner and go to **Users & Permissions -> Roles**.
Click **Add role** and create the following roles one by one:
| Role Name | Role Key | Description |
|-----------|----------|-------------|
| HelpDesk Admin | admin-helpdesk | Can see all tickets, manage categories, assign handlers |
| Technician | technician | Can only see tickets assigned to them, can process and close |
| Regular User | user | Can only submit tickets and view their own submissions |

> The **Role Key** is a unique internal ID used by the system. It cannot be changed after creation, so we recommend using lowercase English. The Role Name can be modified at any time.

After creation, you should see our three new roles in the roles list.
## 5.3 Configuring Menu Permissions
Now that the roles are set up, we need to tell the system which menus each role can access.
Click on a role to enter its permission configuration page, and find the **Menu permissions** tab. This lists all menu items in the system — check a box to allow access, uncheck it to hide it.
**HelpDesk Admin (admin-helpdesk)**: Check all
- Tickets, Categories, Dashboard — all visible
**Technician (technician)**: Partial access
- ✅ Tickets
- ✅ Dashboard
- ❌ Categories (technicians don't need to manage categories)
**Regular User (user)**: Minimum permissions
- ✅ My Tickets (or the "Submit Ticket" page)
- ❌ Tickets
- ❌ Categories
- ❌ Dashboard

> **Tip**: NocoBase has a handy setting — "Allow access to new menu items by default." If you don't want to manually check every new page you add, you can enable this for the admin role. For the regular user role, we recommend keeping it disabled.
## 5.4 Configuring Data Permissions
Menu permissions control "can I access this page?" while data permissions control "what data can I see once I'm on the page?"
Key concept: **[Data Scope](/users-permissions/role/data-scope)**.
In the role's permission settings, switch to the **[Action permissions](/users-permissions/role/action-permissions)** tab. Find our "Tickets" [collection](/data-sources/main/collection) and click to configure it individually.

### Regular User: Only See Their Own Tickets
1. Find the **View** permission for the "Tickets" collection
2. Set the data scope to **Own records**
3. This way, regular users can only see tickets where they are the creator (note: the default "Own records" is based on the system creator field, not the Submitter field, but this can be changed)
Similarly, set the "Edit" and "Delete" permissions to **Own records** as well (or simply don't grant delete permission at all).

About global configuration: if you only configure the Tickets collection, other data and settings (like categories, assignees) may become invisible. Since our system is fairly simple, we'll check "View all data" globally for now, and only set specific data scopes for sensitive collections.

### Technician: Only See Tickets Assigned to Them
1. Find the **View** permission for the "Tickets" collection
2. Set the data scope to **Own records**
3. There's a nuance here — NocoBase's "Own records" filters by creator by default. If we want to filter by "Assignee" instead, we can further adjust this in the global operation permissions, or achieve it on the frontend by setting **filter conditions on the data [block](/interface-builder/blocks)**

> **Practical tip**: You can also set default filter conditions on table blocks to assist permission control, e.g., "Assignee = Current user." But note that page configuration applies globally — admins would be limited too. A compromise: configure "Assignee = Current user **or** Submitter = Current user" to cover both regular users and technicians; if admins need a full view, create a separate page without filter conditions.

### HelpDesk Admin: See All Data
For the admin role, set the data scope to **All records** and enable all operations. Simple and straightforward.

## 5.5 Ticket Assignment Action
Before testing permissions, let's add a handy feature to the ticket list: **assigning a handler**. Admins can assign a ticket to a technician directly from the list, without opening the full edit form and dealing with a bunch of fields.
The implementation is simple — add a custom popup button to the table row actions:
1. Enter [UI Editor](/get-started/how-nocobase-works) mode. In the ticket list table's actions column, click **"+"** to add a **"Popup"** action button.

2. Change the button title to **"Assign"** (click the button settings to modify the title).

Since there's only a simple assignment to make, a compact dialog is more appropriate than a drawer. Click the popup settings in the button's top-right corner, select **Dialog (narrow)** > Confirm.

3. Click the "Assign" button to open the popup. In the popup, go to **"Add block → Data blocks → Form (Edit)"**, and select the current collection.
4. In the form, only check the **"Assignee"** field, and set it as **required** in the field settings.
5. Add a **"Submit"** action button.

Now admins can click "Assign" in the ticket list, pick a handler from a minimal form, and submit. Quick, precise, and no risk of accidentally changing other fields.
### Controlling Button Visibility with Linkage Rules
The "Assign" button is only needed by admins — showing it to regular users and technicians would be confusing. We can use **linkage rules** to show or hide the button based on the current user's role:
1. In UI Editor mode, click the "Assign" button's settings and find **"Linkage rules"**.
2. Add a rule with the condition: **Current user / Roles / Role name** is not equal to **HelpDesk Admin** (the name corresponding to the admin-helpdesk role).
3. Set the action when the condition is met: **Hide** the button.
This way, only users with the admin role can see the "Assign" button — it's automatically hidden for all other roles.

## 5.6 Creating Test Users and Trying It Out
Permissions are configured — let's verify them in practice.
Go to **User Management** (in the settings center or the user management page you built earlier) and create 3 test users:
| Username | Role |
|----------|------|
| Alice | HelpDesk Admin (admin-helpdesk) |
| Bob | Technician (technician) |
| Charlie | Regular User (user) |

After creating them, log in with each account and check two things:
**1. Are the menus displayed as expected?**
- Alice → Can see all menus

- Bob → Only sees Tickets and Dashboard

- Charlie → Only sees "My Tickets"

**2. Is the data filtered as expected?**
- First, log in as Alice and create a few tickets assigned to different people
- Switch to Bob → Only sees tickets assigned to them
- Switch to Charlie → Only sees tickets they submitted
Pretty cool, right? The same system, completely different content for different users! That's the power of permissions.
## Summary
In this chapter, we completed the ticket system's permission framework:
- **3 roles**: HelpDesk Admin, Technician, Regular User
- **Menu permissions**: Control which pages each role can access
- **Data permissions**: Control which data each role can see (via data scope)
- **Test verification**: Log in with different accounts to confirm permissions work
At this point, our ticket system is shaping up nicely — it can handle data entry, viewing, and role-based access control. But everything is still manual.
## Next Chapter Preview
In the next chapter, we'll learn about **Workflows** — letting the system do work for us automatically. For example, automatically notifying the assignee when a ticket is submitted, or logging a timestamp when the status changes.
## Related Resources
- [User Management](/users-permissions/user) — User administration guide
- [Roles & Permissions](/users-permissions/role) — Role configuration reference
- [Data Scope](/users-permissions/role/data-scope) — Row-level permission control
---
url: /tutorials/v2/06-workflows.md
---
# Chapter 6: Workflows — Let the System Do the Work
In the last chapter, we added permissions so different roles see different content. But all operations are still done manually — when a new ticket comes in, someone has to go check; when a status changes, nobody gets notified.
In this chapter, we'll use NocoBase's [Workflow](/workflow) engine to make the system **do things automatically** — configure [condition](/workflow/nodes/condition) checks and [update](/workflow/nodes/update) nodes for automatic ticket status transitions and timestamp recording.
## 6.1 What Is a Workflow?
A workflow is a set of automated "if... then..." rules.
Think of it like an alarm on your phone that goes off every morning at 8 AM. That alarm is the simplest workflow — **when a condition is met (it's 8 AM), an action executes automatically (the alarm rings)**.
NocoBase workflows follow the same idea:

- **[Trigger](/workflow/triggers/collection)**: The entry point for the workflow. For example, "someone created a new ticket" or "a record was updated"
- **Condition**: An optional filtering step. For example, "only continue if the assignee is not empty"
- **Action**: The step that does the actual work. For example, "send a notification" or "update a [field](/data-sources/field)"
Actions can be chained with multiple nodes. Common node types include:
- **Flow control**: Condition, Parallel [branch](/workflow/nodes/condition), Loop, Delay
- **Data operations**: [Create record](/workflow/nodes/create), Update record, Query record, Delete record
- **Notifications & external**: Notification, HTTP request, Calculation
This tutorial only covers a few of the most common ones — once you learn to combine them, you'll be able to handle most scenarios.
### Trigger Types at a Glance
NocoBase offers several trigger types, which you select when creating a workflow:
| Trigger | Description | Typical Use Case |
|---------|-------------|-----------------|
| **[Collection event](/workflow/triggers/collection)** | Fires when a record is created, updated, or deleted | New ticket notification, status change logging |
| **[Schedule](/workflow/triggers/schedule)** | Fires on a Cron expression or fixed time | Daily reports, periodic data cleanup |
| **[Post-action event](/workflow/triggers/action)** | Fires after a user performs a UI action | Send notification after form submission |
| **Approval** | Initiates an approval flow with multi-level support | Leave requests, purchase approvals |
| **Custom action** | Bound to a custom button, fires on click | One-click archiving, batch operations |
| **Pre-action event** | Intercepts a user action synchronously before it completes | Pre-submit validation, auto-fill fields |
| **AI Employee** | Exposes the workflow as a tool for AI employees to call | AI-driven business operations |
This tutorial uses both **Collection event** and **Custom action event** triggers. The other types work similarly; once you've learned these, you can pick up the rest quickly.
NocoBase workflows are a built-in plugin — no extra installation needed, ready to use out of the box.
## 6.2 Scenario 1: Automatically Notify the Assignee on New Tickets
**Requirement**: When someone creates a new ticket and specifies an assignee, the system automatically sends an in-app message to the assignee, letting them know "there's work to do."
### Step 1: Create a Workflow
Open the plugin settings menu in the top-right corner and go to **Workflow management**.

Click **New**, and in the dialog that appears:
- **Name**: Enter "Notify assignee on new ticket"
- **Trigger type**: Select **Collection event**

After submitting, click the **Configure** link in the list to enter the flow editor.
### Step 2: Configure the Trigger
Click the trigger card at the top to open its configuration drawer:
- **[Collection](/data-sources/main/collection)**: Select Main datasource / "Tickets"
- **Trigger on**: Select "After record created or updated"
- **Changed fields**: Check "Assignee" — the workflow only triggers when the Assignee field changes, avoiding unnecessary notifications from other field updates (when creating a record, all fields are considered changed, so new tickets will also trigger)
- **Only triggers when conditions are met**: Set the mode to "Match **any** condition in the group," then add two conditions:
- `assignee_id` is not empty
- `Assignee / ID` is not empty
> Why two conditions? Because at trigger time, the form may only have the foreign key (assignee_id) without the associated object loaded, or vice versa. Using OR ensures the workflow triggers as long as an assignee is specified.
- **Preload associations**: Check "Assignee" — the notification node needs the assignee's information, so it must be preloaded in the trigger

Click Save. The trigger itself now handles the condition filtering — it only fires when the Assignee is not empty, so there's no need for a separate condition node.
### Step 3: Add a Notification Node
Click the **+** below the trigger and select a **Notification** node.

Open the notification node's configuration. The first option is to select a **Notification channel** — but we haven't created one yet, so the dropdown is empty. Let's go create one first.

### Step 4: Create a Notification Channel
NocoBase supports multiple notification channel types:
| Channel Type | Description |
|-------------|-------------|
| **In-app message** | Browser notifications, pushed in real-time to the user's notification center |
| **Email** | Sends via SMTP, requires email server configuration |
For this tutorial, we'll use the simplest option — **In-app message**:
1. Open the plugin settings in the top-right corner and go to **Notification management**
2. Click **Create channel**

3. Select **In-app message** as the channel type, and enter a name (e.g., "System In-App Messages")
4. Save

### Step 5: Configure the Notification Node
Go back to the workflow editor and open the notification node's configuration.
The notification node has the following configuration options:
- **Notification channel**: Select the "System In-App Messages" channel you just created
- **Receivers**: Click to select Query users → set `id = Trigger variable / Trigger data / Assignee / ID`
- **Title**: Enter a notification title, e.g., "You have a new ticket to handle." Supports variables — for example, include the ticket title: `New ticket: {{Trigger data / Title}}`
- **Content**: Enter the notification body. You can also insert variables to reference the ticket's priority, description, and other fields

(Before leaving the popup for the next step, remember to save first!)
- **Desktop detail page**: Enter the URL path of the ticket detail page. To get it: open any ticket's detail popup in the frontend, then copy the path from the browser address bar — it looks like `/admin/camcwbox2uc/view/d8f8e122d37/filterbytk/353072988225540`. Paste the path into the field, then replace the number after `filterbytk/` with the trigger data's ID variable (click the variable selector → Trigger data → ID). Once configured, clicking the notification in the list will navigate directly to that ticket's detail page and automatically mark it as read


- **Continue on failure**: Optional. If checked, the workflow won't stop even if the notification fails to send
> After the notification is sent, the assignee can see the message in the **Notification center** (top-right corner of the page). Unread items will show a red dot indicator. Clicking the notification takes you directly to the ticket detail page.
### Step 6: Test and Enable
> The complete flow for Scenario 1 has just two nodes: Trigger (with condition filtering) → Notification. Simple and direct.
Don't rush to enable it — workflows provide a **manual execution** feature that lets you test the flow with specific data first:
1. Click the **Execute** button in the top-right corner (not the enable toggle)
2. Select an existing ticket record as the trigger data
> If the ticket selector shows IDs instead of titles, go to Data sources → Collections → Tickets and set the "Title" column as the title field.

3. Click execute. The workflow will run and automatically switch to a duplicated new version.

4. Click the three dots in the top-right corner and select Execution history. You should see the execution record we just created. Click to view the details — including trigger info, each node's execution details, and parameters.


5. The ticket we just tested seems to be assigned to Alice. Let's switch to Alice's account — notification received!

Click the notification to jump to the target ticket page. The notification is automatically marked as read.

Once confirmed, click the **On/Off** toggle in the top-right corner to enable the workflow.

> **Note**: Once a workflow has been executed (including manual execution), it becomes **read-only** (grayed out) and can no longer be edited. If you need to make changes, click **"Duplicate to new version"** in the top-right corner and continue editing the new version. The old version is automatically disabled.

Go back to the tickets page and create a new ticket — make sure to select an assignee. Then switch to the assignee's account and check the notification center — you should see a new notification.

Congratulations, your first automation is up and running!
## 6.3 Scenario 2: Automatically Record Completion Time
**Requirement**: When a ticket is marked as "Completed," the system automatically fills in the "Completion time" field with the current time. No manual entry needed, and it never gets forgotten.
> If you haven't created a "Completion time" field in the Tickets collection yet, go to **Collection management → Tickets** and add a **Date** type field named "Completion time." Refer to Chapter 2 for how to create fields — we won't repeat the steps here.
> 
### Step 1: Create a Workflow
Go back to the workflow management page and click New:
- **Name**: Enter "Auto-record ticket completion time"
- **Trigger type**: Select **Custom action event** (fires when a user clicks a button bound to this workflow)
- **Execution mode**: Synchronous
> About synchronous vs asynchronous:
> - Asynchronous: After the action, you can continue doing other things while the workflow runs in the background
> - Synchronous: After the action, the UI waits for the workflow to finish before you can do anything else

### Step 2: Configure the Trigger
Open the trigger configuration:
- **Collection**: Select "Tickets"
- **Execution mode**: Select **Single record mode** (processes only the current ticket each time)

### Step 3: Add a Condition
Unlike the collection event trigger which has built-in conditions, we need to add a condition node ourselves:

We recommend selecting "Continue on 'Yes' and 'No' separately" for easier future expansion.
- Condition: **Trigger data → Status** does not equal **Completed** (only incomplete tickets pass through; already completed tickets won't be processed again)

### Step 4: Add an Update Record Node
On the "Yes" branch of the condition, click **+** and select an **Update record** node:

- **Collection**: Select "Tickets"
- **Filter condition**: ID equals Trigger data → ID (to ensure only the current ticket gets updated)
- **Field values**:
- Status = **Completed**
- Completion time = **System variables / System time**

> This way, a single node handles both "change status" and "record time" — no need to configure field values on the button separately.
### Step 5: Create a "Complete" Action Button
The workflow is configured, but a "Custom action event" needs to be bound to a specific action button to trigger. Let's create a dedicated "Complete" button in the ticket list's action column:
1. Enter UI Editor mode. In the ticket table's action column, click **"+"** and select a **"Trigger workflow"** action button

2. Click the button settings and change the title to **"Complete"**, then select a completion-related icon (e.g., a checkmark icon)

3. Configure **linkage rules** for the button: hide it when the ticket status is already "Completed" (completed tickets don't need a "Complete" button)
- Condition: Current data → Status equals Completed
- Action: Hide

4. Open **"Bind workflows"** in the button settings and select the "Auto-record ticket completion time" workflow we just created

### Step 6: Configure Event Flow Refresh
The button is created, but the table won't automatically refresh after clicking — users won't see the status change. We need to configure the button's **event flow** to automatically refresh the table after the workflow completes.
1. Click the second lightning bolt icon (⚡) in the button settings to open the **Event flow** configuration
2. Configure the trigger event:
- **Trigger event**: Select **Click**
- **Execution timing**: Select **After all flows**
3. Click **"Append step"** and select **"Refresh target block"**

4. Find the ticket table on the current page, open its settings menu, and click **"Copy UID"** at the bottom. Paste the UID into the refresh step's target block field

This way, after clicking the "Complete" button, the table automatically refreshes once the workflow finishes, and users immediately see the status and completion time changes.
### Step 7: Enable and Test
Go back to the workflow management page and enable the "Auto-record ticket completion time" workflow.
Then open a ticket with "In Progress" status and click the **"Complete"** button in the action column. You should see:
- The ticket's "Completion time" field is automatically filled with the current time
- The table refreshes automatically, and the "Complete" button has disappeared for this ticket (linkage rule in effect)

Convenient, right? This is the second common use case for workflows — **automatically updating data**. And by using the "Custom action event + Button binding" approach, we've created a precise trigger mechanism: the workflow only runs when a specific button is clicked.
## 6.4 Viewing Execution History
How many times has the workflow run? Were there any errors? NocoBase keeps track of everything.
In the workflow management list, each workflow shows an **Execution count** link. Click it to see the detailed record of each execution:
- **Execution status**: Success (green) or failure (red) — easy to spot at a glance
- **Trigger time**: When was it triggered
- **Node details**: Click in to see the execution result of each node

If an execution failed, clicking into the details shows which node had the problem and the specific error message. This is the most important tool for debugging workflows.

## Summary
In this chapter, we created two simple but practical workflows:
- **New ticket notification** (Collection event trigger): Automatically notifies the assignee when a ticket is created or reassigned — no more yelling across the office
- **Auto-record completion time** (Custom action event trigger): Click "Complete" and the timestamp is filled automatically — no more human oversight
These two workflows demonstrate two different trigger types, and together took less than 10 minutes to configure. The system can already do things automatically. NocoBase supports many more node types (HTTP requests, calculations, loops, etc.), but for getting started, mastering these combos is enough to handle most scenarios.
## Next Chapter Preview
The system can do things automatically now, but we're still missing a "big picture view" — how many tickets are there in total? Which category has the most? How many new tickets per day? In the next chapter, we'll use chart [blocks](/interface-builder/blocks) to build a **data dashboard** to see everything at a glance.
## Related Resources
- [Workflow Overview](/workflow) — Core workflow concepts and use cases
- [Collection Event Trigger](/workflow/triggers/collection) — Data change trigger configuration
- [Update Record Node](/workflow/nodes/update) — Automatic data update setup
---
url: /tutorials/v2/07-dashboard.md
---
# Chapter 7: Dashboard — The Big Picture at a Glance
In the last chapter, we used workflows to make the system send notifications and record timestamps automatically. The system is getting smarter, but we're still missing one thing — **a bird's-eye view**.
How many tickets are there? How many have been resolved? Which category has the most issues? How many new tickets come in each day? You can't answer these questions by scrolling through a list. In this chapter, we'll use [chart blocks](/data-visualization) (pie, line, bar charts) and [Markdown blocks](/interface-builder/blocks/other/markdown) to build a **data dashboard** that turns raw data into something you can understand at a glance.
## 7.1 Adding a Dashboard Page
First, let's add a new [menu](/interface-builder/menus) item to the top navigation bar.
Enter [configuration mode](/get-started/how-nocobase-works), click **"Add menu item"** (`+` icon) on the top menu bar, select **"Modern page (v2)"**, and name it "Dashboard."

This [page](/interface-builder/pages) is dedicated to charts — it's our dashboard home base.
## 7.2 Pie Chart: Ticket Status Distribution
For our first chart, we'll use a pie chart to show how many tickets are "Pending," "In Progress," and "Completed."
On the Dashboard page, click **Add block → [Chart](/data-visualization)**.
After adding it, click the **Configure** button in the top-right corner of the [block](/interface-builder/blocks). A chart configuration panel will open on the right side.
### Configuring the Data Query
- **[Collection](/data-sources/main/collection)**: Select "Tickets"
- **Measures**: Select any unique [field](/data-sources/field) (e.g., ID), set the aggregation to **Count**
- **Dimensions**: Select the "Status" field

Click **Run query** to preview the returned data below.
### Configuring Chart Options
- **Chart type**: Select **Pie**
- **Field mapping**: Set Category to "Status" and Value to the count value
- **Labels**: Turn on the toggle
A nice-looking pie chart should now appear on the page. Each slice represents a status, showing the exact count and percentage by default.

Click **Save** — your first chart is done.
## 7.3 Line Chart: Daily New Ticket Trend
The pie chart shows "how things are distributed right now." A line chart shows "how things change over time."
Add another chart block to the page with the following configuration:
### Data Query
- **Collection**: Select "Tickets"
- **Measures**: ID, Count
- **Dimensions**: Select the "Created at" field, set the format to **YYYY-MM-DD** (group by day)
> **Tip**: The date dimension format matters. Choosing `YYYY-MM-DD` groups by day; choosing `YYYY-MM` groups by month. Pick the right granularity based on your data volume.
### Chart Options
- **Chart type**: Select **Line**
- **Field mapping**: Set xField to "Created at" and yField to the count value

After saving, you'll see how ticket volume changes over time. If there's a sudden spike on a particular day, something happened worth looking into.
## 7.4 Bar Chart: Tickets by Category
For our third chart, let's see which category has the most tickets. We'll use a **horizontal bar chart** instead of a vertical column chart — when there are many categories, vertical X-axis labels tend to overlap and get hidden, so horizontal display is much clearer.
Add a third chart block:
### Data Query
- **Collection**: Select "Tickets"
- **Measures**: ID Count
- **Dimensions**: Select the "Category" relation field (choose the category's Name field)
### Chart Options
- **Chart type**: Select **Bar**
- **Field mapping**: Set xField to the count value (ID Count) and yField to "Category Name"

After saving, it's immediately clear which category has the most issues. If the "Network Failure" bar stretches far beyond the rest, maybe it's time to upgrade the network equipment.
## 7.5 Table Block: Unresolved Tickets
Charts give a summary view, but admins usually need to see specific details too. Let's add an **Unresolved Tickets** table that shows all tickets that haven't been completed yet.
Add a **Table block** to the page, selecting the "Tickets" collection.
### Configure Filter Conditions
Click the table block's settings and find **Set data scope**. Add a [filter](/interface-builder/blocks/filter-blocks/form) condition:
- **Status** is not equal to **Completed**
This way the table only shows unfinished tickets — once a ticket is completed, it automatically disappears from the list.
### Configure Fields
Select the columns to display: Title, Status, Priority, Assignee, Created at.

> **Tip**: You can also add a **default sort** (by Created at, descending) so the newest tickets appear at the top.
## 7.6 Markdown Block: System Announcements
Beyond charts, we can also put some text information on the dashboard.
Add a **[Markdown block](/interface-builder/blocks/other/markdown)** and write a system announcement or usage instructions:
```markdown
## IT HelpDesk System
Welcome! If you run into any issues, please submit a ticket and the tech team will handle it ASAP.
**For urgent issues**, call the IT hotline directly: 8888
```

Place the Markdown block at the top of the dashboard — it works as both a welcome message and a bulletin board. The content can be updated anytime, making it very flexible.
## 7.7 JS Block: Personalized Welcome Banner
Markdown has a fairly fixed format — what if you want richer effects? NocoBase provides a **JS Block (JavaScript Block)** that lets you freely customize display content with code.
We'll use it to create a business-style welcome banner that shows a personalized greeting based on the current user and time of day.
Add a **JS block** to the page (Add block → Other blocks → JS block).

In the JS block, you can use `ctx.getVar("ctx.user.username")` to get the current user's username. Here's a clean, business-style welcome banner:
```js
const uname = await ctx.getVar("ctx.user.username")
const name = uname || 'User';
const hour = new Date().getHours();
const hi = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
const d = new Date();
const date = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
const weekDay = d.toLocaleDateString('en-US', { weekday: 'long' });
ctx.render(`
${hi}, ${name}
Welcome back to IT HelpDesk
${date} ${weekDay}
`);
```

The result is a light gray card with the greeting on the left and date on the right. Clean, practical, and unobtrusive.
> **Tip**: `ctx.getVar("ctx.user.xxx")` is how you access current user info in JS blocks. Common fields include `nickname`, `username`, and `email`. JS blocks can also call APIs to query data — you can use them for much more custom content later.
## 7.8 Action Panel: Quick Links + Popup Reuse
A dashboard isn't just for viewing data — it should also be a starting point for actions. Let's add an **Action Panel** so users can submit tickets and jump to the ticket list directly from the homepage.
Add an **Action Panel** block (Add block → Other blocks → Action Panel), then add two [actions](/interface-builder/actions) inside it:

1. **Link: Go to ticket list** — Add a "Link" action and configure the URL to point to the ticket list page (e.g., `/admin/camcwbox2uc`)

2. **Button: Add Ticket** — Add a "Popup" action button and change the title to "Add Ticket"

But clicking "Add Ticket" opens an empty popup — we need to configure its content. Rebuilding the entire new ticket form from scratch would be tedious. This is where a very handy feature comes in: **Popup Reuse**.
### Saving a Popup Template
> Note: Popup templates are different from the "block templates" we covered in Chapter 4. Block templates save a single form block's fields and layout, while popup templates save the **entire popup** — including all blocks, fields, and action buttons inside it.
1. Go to the **Tickets list page** and find the "Add Ticket" button
2. Click the button's settings and find **"Save as template"** — save the current popup
3. Give the template a name (e.g., "New Ticket Popup")

### Reusing the Popup on the Dashboard
1. Go back to the Dashboard page and click the "Add Ticket" button's settings in the action panel
2. Find **"Popup settings"** and select the "New Ticket Popup" template you just saved
3. After saving, clicking the button will open the exact same new ticket form popup as on the ticket list page


### Click Title to Open Detail Popup
Using the same approach, we can make ticket titles in the unresolved tickets table clickable to open the detail view directly:
1. First, go to the **Tickets list page**, find the "View" button's settings, and similarly **"Save as template"** (e.g., "Ticket Detail Popup")

2. Go back to the Dashboard page. In the unresolved tickets table, click the "Title" field's settings
3. Turn on the **"Enable click to open"** toggle — a "Popup settings" option will appear
4. In the popup settings, select the "Ticket Detail Popup" template you just saved

Now users can click a ticket title on the dashboard to view its details instantly, without navigating to the ticket list page. The whole dashboard becomes more compact and efficient.

> **Benefits of popup reuse**: The same popup template can be used across multiple pages. When you modify the template, all references update automatically. This is similar to the "Reference" mode from Chapter 4 — maintain in one place, apply everywhere.
## 7.9 Adjusting the Layout
We now have 6 blocks on the page (JS welcome banner + Action panel + 3 charts + tickets table). Let's adjust the layout to make it look better.
In configuration mode, you can **drag and drop** to reposition and resize each block. Suggested layout:
- **Row 1**: JS welcome banner (left) + Action panel (right)
- **Row 2**: Pie chart (left) + Tickets table (right)
- **Row 3**: Line chart (left) + Bar chart (right)

Note: you may find that block heights don't align. You can manually adjust this in Block settings → Block height — for example, set both blocks in row 2 to 500px.
Drag the edges to adjust block widths so the two charts each take up half the row. Try a few arrangements until you find what looks best.

## Summary
In this chapter, we built a rich and practical data dashboard with 6 blocks:
- **JS welcome banner**: Personalized greeting based on current user and time
- **Action panel**: Quick link to ticket list + one-click ticket creation (popup reuse)
- **Pie chart**: See the ticket status distribution at a glance
- **Line chart**: Track how ticket volume changes over time
- **Bar chart**: Compare ticket counts across categories horizontally — no label overlap even with many categories
- **Unresolved tickets table**: All pending tickets at a glance, click title to view details (popup reuse)
We also learned an important technique — **Popup Reuse**: save a popup from one page as a template and reference it on other pages, avoiding repetitive configuration.
Data visualization is a built-in NocoBase plugin — no additional installation needed. Configuring it is just as simple as building a page: pick your data, choose a chart type, map the fields — three steps and you're done.
## What's Next
At this point, our ticket system is quite feature-complete: data modeling, page building, form entry, access control, automated workflows, and a data dashboard — we've got it all. We're planning an **AI Agent version of this tutorial** — using AI Agents to build the system locally and automatically. Stay tuned.
## Related Resources
- [Data Visualization](/data-visualization) — Chart configuration guide
- [Markdown Block](/interface-builder/blocks/other/markdown) — Markdown block usage
- [Block Layout](/interface-builder/blocks) — Page layout and block configuration
---
url: /data-visualization/guide/data-query.md
---
# Data query
The chart configuration panel is divided into three sections: Data query, Chart options, and Interaction events, plus Cancel, Preview, and Save buttons at the bottom.
Let's first look at the "Data query" panel to understand the two query modes (Builder/SQL) and their common features.
## Panel Structure

> Tips: To configure the current content more easily, you can collapse other panels first.
The top is the action bar:
- Mode: Builder (graphical, simple, and convenient) / SQL (handwritten statements, more flexible).
- Run query: Click to execute the data query request.
- View result: Opens the data result panel, where you can switch between Table/JSON views. Click again to collapse the panel.
From top to bottom:
- Data source and collection: Required. Select the data source and collection.
- Measures: Required. The numeric fields to be displayed.
- Dimensions: Group by fields (e.g., date, category, region).
- Filter: Set filter conditions (e.g., =, ≠, >, <, contains, range). Multiple conditions can be combined.
- Sort: Select the field to sort by and the order (ascending/descending).
- Pagination: Control the data range and return order.
## Builder mode
### Select data source and collection
- In the "Data query" panel, set the mode to "Builder".
- Select a data source and collection. If the collection is not selectable or is empty, first check permissions and whether it has been created.
### Configure Measures
- Select one or more numeric fields and set an aggregation: `Sum`, `Count`, `Avg`, `Max`, `Min`.
- Common use cases: `Count` to count records, `Sum` to calculate a total.
### Configure Dimensions
- Select one or more fields as grouping dimensions.
- Date and time fields can be formatted (e.g., `YYYY-MM`, `YYYY-MM-DD`) to facilitate grouping by month or day.
### Filter, Sort, and Pagination
- Filter: Add conditions (e.g., =, ≠, contains, range). Multiple conditions can be combined.
- Sort: Select a field and the sort order (ascending/descending).
- Pagination: Set `Limit` and `Offset` to control the number of returned rows. It's recommended to set a small `Limit` when debugging.
### Run Query and View Result
- Click "Run query" to execute. After it returns, switch between `Table / JSON` in "View result" to check the columns and values.
- Before mapping chart fields, confirm the column names and types here to avoid an empty chart or errors later.

### Subsequent Field Mapping
Later, when configuring "Chart options", you will map fields based on the fields from the selected data source and collection.
## SQL mode
### Write Query
- Switch to "SQL" mode, enter your query statement, and click "Run query".
- Example (total order amount by date):
```sql
SELECT
TO_CHAR(order_date, 'YYYY-MM') as mon,
SUM(total_amount) AS total
FROM "order"
GROUP BY mon
ORDER BY mon ASC
LIMIT 100;
```

### Run Query and View Result
- Click "Run query" to execute. After it returns, switch between `Table / JSON` in "View result" to check the columns and values.
- Before mapping chart fields, confirm the column names and types here to avoid an empty chart or errors later.
### Subsequent Field Mapping
Later, when configuring "Chart options", you will map fields based on the columns from the query result.
> [!TIP]
> For more information on SQL mode, please see Advanced Usage — Query Data in SQL Mode.
---
url: /data-visualization/guide/chart-options.md
---
# Chart options
Configure how charts are displayed. Two modes are supported: Basic (visual) and Custom (JS). Basic is ideal for quick mapping and common properties; Custom fits complex scenarios and advanced customization.
## Panel layout

> Tips: To configure more easily, collapse other panels first.
Top action bar
Mode selection:
- Basic: Visual configuration. Choose a type and complete field mapping; adjust common properties with switches.
- Custom: Write JS in the editor and return an ECharts `option`.
## Basic mode

### Choose chart type
- Supported: line, area, column, bar, pie, donut, funnel, scatter, etc.
- Required fields vary by chart type. First confirm column names and types in “Data query → View data”.
### Field mapping
- Line/area/column/bar:
- `xField`: dimension (date, category, region)
- `yField`: measure (aggregated numeric value)
- `seriesField` (optional): series grouping (for multiple lines/groups)
- Pie/donut:
- `Category`: categorical dimension
- `Value`: measure
- Funnel:
- `Category`: stage/category
- `Value`: value (usually count or percentage)
- Scatter:
- `xField`, `yField`: two measures or dimensions for axes
> For more chart options, refer to the ECharts docs: [Axis](https://echarts.apache.org/handbook/en/concepts/axis) and [Examples](https://echarts.apache.org/examples/en/index.html)
**Notes:**
- After changing dimensions or measures, recheck the mapping to avoid empty or misaligned charts.
- Pie/donut and funnel must provide a “category + value” combination.
### Common properties

- Stack, smooth (line/area)
- Labels, tooltip, legend
- Axis label rotation, split lines
- Pie/donut radius and inner radius, funnel sort order
**Recommendations:**
- Use line/area for time series with moderate smoothing; use column/bar for category comparison.
- With dense data, avoid showing all labels to prevent overlap.
## Custom mode
Return a full ECharts `option`. Suitable for advanced customization such as merging multiple series, complex tooltips, and dynamic styles. Recommended approach: consolidate data in `dataset.source`. For details, see ECharts docs: [Dataset](https://echarts.apache.org/handbook/en/concepts/dataset/#map-row-or-column-of-dataset-to-series)

### Data context
- `ctx.data.objects`: array of objects (each row as an object, recommended)
- `ctx.data.rows`: 2D array (with header)
- `ctx.data.columns`: 2D array grouped by columns
### Example: monthly orders line chart
```js
return {
dataset: { source: ctx.data.objects || [] },
xAxis: { type: 'category' },
yAxis: {},
series: [
{
type: 'line',
smooth: true,
showSymbol: false,
},
],
}
```
### Preview and Save
- In Custom mode, after editing, click the Preview button on the right to update the chart preview.
- At the bottom, click Save to apply and persist; click Cancel to revert all changes made this time.

> [!TIP]
> For more on chart options, see Advanced — Custom chart configuration.
---
url: /data-visualization/guide/preview-and-save.md
---
# Preview and Save
- Preview: Temporarily render changes from the configuration panel into the page chart to verify the result.
- Save: Persist changes from the configuration panel to the database.
## Entry points

- In visual (Basic) mode, changes are applied to the preview automatically by default.
- In SQL and Custom modes, click the Preview button on the right to apply changes to the preview.
- A unified Preview button is available at the bottom of the configuration panel.
## Preview behavior
- Temporarily displays the configuration on the page without writing to the database. After a refresh or cancel, the preview result is not retained.
- Built‑in debounce: multiple refresh triggers in a short time only execute the latest one to avoid frequent requests.
- Clicking Preview again overwrites the last preview result.
## Error messages
- Query errors or validation failures: shown in the View data area.
- Chart configuration errors (missing Basic mapping, exceptions from Custom JS): shown in the chart area or console while keeping the page operable.
- Confirm column names and data types in View data before field mapping or writing Custom code to reduce errors.
## Save and Cancel
- Save: write current changes into the block configuration and apply them to the page immediately.
- Cancel: discard current unsaved changes and revert to the last saved state.
- Save scope:
- Data query: Builder parameters; in SQL mode, the SQL text is saved as well.
- Chart options: Basic type, field mapping, and properties; Custom JS text.
- Interaction events: JS text and binding logic.
- After saving, the block takes effect for all visitors (subject to page permissions).
## Recommended flow
- Configure data query → Run query → View data to confirm column names and types → Configure chart options to map core fields → Preview to validate → Save to apply.
---
url: /data-visualization/guide/context-variables.md
---
# Use context variables
With context variables, you can reuse information from the current page, user, time, and filter inputs to render charts and enable linkage based on context.
## Applicable scope
- Data query in Builder mode: select variables for filter conditions.

- Data query in SQL mode: choose variables and insert expressions (for example, `{{ ctx.user.id }}`).

- Chart options in Custom mode: write JS expressions directly.

- Interaction events (for example, click to open a drill‑down dialog and pass data): write JS expressions directly.

**Note:**
- Do not wrap `{{ ... }}` with single or double quotes; binding is handled safely based on variable type (string, number, time, NULL).
- When a variable is `NULL` or undefined, handle nulls explicitly in SQL using `COALESCE(...)` or `IS NULL`.
---
url: /data-visualization/guide/filters-and-linkage.md
---
# Page filters and linkage
The page filter (filter block) provides a unified input for filter conditions at the page level and merges them into chart queries to keep multiple charts filtered consistently and linked.
## Feature overview
- Add a filter block to the page to provide a unified filter entry for all charts.
- Use “Filter”, “Reset”, and “Collapse” buttons to apply, clear, and collapse.
- If the filter selects fields associated with a chart, their values are automatically merged into the chart query and trigger a refresh.
- Filters can define custom fields and register them in context variables so they can be referenced in charts, tables, forms, and other data blocks.

For more on using page filters and linking with charts or other data blocks, see the page filter documentation.
## Use page filter values in chart queries
- Builder mode (recommended)
- Auto merge: When the data source and collection match, you do not need to write variables in the chart query; page filters are merged with `$and`.
- Manual selection: You can also select values from filter block custom fields in chart filter conditions.
- SQL mode (via variable injection)
- In SQL, use “Choose variable” to insert values from filter block custom fields.
---
url: /data-visualization/guide/sql-data-query.md
---
# Query data in SQL mode
In the Data query panel, switch to SQL mode, write and run the query, and use the returned result directly for chart mapping and rendering.

## Write SQL statements
- In the Data query panel, choose SQL mode.
- Enter SQL and click Run query.
- Supports complex statements including multi‑table JOIN and VIEW.
Example: order amount by month
```sql
SELECT
TO_CHAR(order_date, 'YYYY-MM') as mon,
SUM(total_amount) AS total
FROM "order"
GROUP BY mon
ORDER BY mon ASC
LIMIT 100;
```
## View results
- Click View data to open the preview panel.

Data supports pagination; you can switch between Table and JSON to check column names and types.

## Field mapping
- In Chart options, map fields based on the query result columns.
- By default, the first column is used as the dimension (X axis or category), and the second column as the measure (Y axis or value). Mind the column order in SQL:
```sql
SELECT
TO_CHAR(order_date, 'YYYY-MM') as mon, -- dimension field in the first column
SUM(total_amount) AS total -- measure field afterwards
```

## Use context variables
Click the x button at the top‑right of the SQL editor to choose context variables.

After confirming, the variable expression is inserted at the cursor (or replaces the selected text).
For example, `{{ ctx.user.createdAt }}`. Do not add extra quotes.

## More examples
For more examples, see the NocoBase [Demo app](https://demo3.sg.nocobase.com/admin/5xrop8s0bui)
**Recommendations:**
- Stabilize column names before mapping to charts to avoid errors later.
- During debugging, set `LIMIT` to reduce returned rows and speed up preview.
## Preview, save, and rollback
- Click Run query to request data and refresh the chart preview.
- Click Save to persist the current SQL text and related configuration to the database.
- Click Cancel to revert to the last saved state and discard current unsaved changes.
---
url: /data-visualization/guide/custom-chart-options.md
---
# Custom chart configuration
In Custom mode, configure charts by writing JS in the editor. Based on `ctx.data`, return a complete ECharts `option`. This suits merging multiple series, complex tooltips, and dynamic styles. In principle, all ECharts features and chart types are supported.

## Data context
- `ctx.data.objects`: array of objects (each row as an object)
- `ctx.data.rows`: 2D array (with header)
- `ctx.data.columns`: 2D array grouped by columns
**Recommended usage:**
Consolidate data in `dataset.source`. For detailed usage, please refer to the ECharts documentation:
[Dataset](https://echarts.apache.org/handbook/en/concepts/dataset/#map-row-or-column-of-dataset-to-series)
[Axis](https://echarts.apache.org/handbook/en/concepts/axis)
[Examples](https://echarts.apache.org/examples/en/index.html)
Let’s start with a simple example.
## Example 1: Monthly order bar chart

```js
return {
dataset: { source: ctx.data.objects || [] },
xAxis: { type: 'category' },
yAxis: {},
series: [
{
type: 'bar',
showSymbol: false,
},
],
}
```
## Example 2: Sales trend chart

```js
return {
dataset: {
source: ctx.data.objects.reverse()
},
title: {
text: "Monthly Sales Trend",
subtext: "Last 12 Months",
left: "center"
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross"
}
},
legend: {
data: ["Revenue", "Order Count", "Avg Order Value"],
bottom: 0
},
grid: {
left: "5%",
right: "5%",
bottom: "60",
top: "80",
containLabel: true
},
xAxis: {
type: "category",
boundaryGap: false,
axisLabel: {
rotate: 45
}
},
yAxis: [
{
type: "value",
name: "Amount(¥)",
position: "left",
axisLabel: {
formatter: (value) => {
return (value/10000).toFixed(0) + '0k';
}
}
},
{
type: "value",
name: "Order Count",
position: "right"
}
],
series: [
{
name: "Revenue",
type: "line",
smooth: true,
encode: {
x: "month",
y: "monthly_revenue"
},
areaStyle: {
opacity: 0.3
},
itemStyle: {
color: "#5470c6"
}
},
{
name: "Order Count",
type: "bar",
yAxisIndex: 1,
encode: {
x: "month",
y: "order_count"
},
itemStyle: {
color: "#91cc75",
opacity: 0.6
}
},
{
name: "Avg Order Value",
type: "line",
encode: {
x: "month",
y: "avg_order_value"
},
itemStyle: {
color: "#fac858"
},
lineStyle: {
type: "dashed"
}
}
]
}
```
**Recommendations:**
- Keep a pure function style: generate `option` only from `ctx.data` and avoid side effects.
- Changes to query column names affect indexing; standardize names and confirm in "View data" before editing code.
- For large datasets, avoid complex synchronous computations in JS; aggregate during the query stage when necessary.
## More examples
For more usage examples, you can refer to the NocoBase [Demo app](https://demo3.sg.nocobase.com/admin/5xrop8s0bui).
You can also browse the official ECharts [Examples](https://echarts.apache.org/examples/en/index.html) to find your desired chart effect, then reference and copy the JS configuration code.
## Preview and Save

- Click "Preview" on the right side or at the bottom to refresh the chart and validate the JS configuration.
- Click "Save" to persist the current JS configuration to the database.
- Click "Cancel" to revert to the last saved state.
---
url: /data-visualization/guide/chart-events.md
---
# Custom interaction events
Write JS in the events editor and register interactions via the ECharts instance `chart` to enable linkage, such as navigating to a new page or opening a drill-down dialog.

## Register and Unregister
- Register: `chart.on(eventName, handler)`
- Unregister: `chart.off(eventName, handler)` or `chart.off(eventName)` to clear events by name
**Note:**
For safety, it's strongly recommended to unregister an event before registering it again!
## Handler params structure

Common fields include `params.data` and `params.name`.
## Example: click to highlight selection
```js
chart.off('click');
chart.on('click', (params) => {
const { seriesIndex, dataIndex } = params;
// Highlight the current data point
chart.dispatchAction({ type: 'highlight', seriesIndex, dataIndex });
// Downplay others
chart.dispatchAction({ type: 'downplay', seriesIndex });
});
```
## Example: click to navigate
```js
chart.off('click');
chart.on('click', (params) => {
const order_date = params.data[0]
// Option 1: internal navigation without full page refresh (recommended), only need relative path
ctx.router.navigate(`/new-path/orders?order_date=${order_date}`)
// Option 2: navigate to external page, full URL required
window.location.href = `https://www.host.com/new-path/orders?order_date=${order_date}`
// Option 3: open external page in a new tab, full URL required
window.open(`https://www.host.com/new-path/orders?order_date=${order_date}`)
});
```
## Example: click to open details dialog (drill-down)
```js
chart.off('click');
chart.on('click', (params) => {
ctx.openView(ctx.model.uid + '-1', {
mode: 'dialog',
size: 'large',
defineProperties: {}, // register context variables for the new dialog
});
});
```

In the newly opened dialog, use chart context variables via `ctx.view.inputArgs.XXX`.
## Preview and Save
- Click "Preview" to load and execute the event code.
- Click "Save" to persist the current event configuration.
- Click "Cancel" to revert to the last saved state.
**Recommendations:**
- Always use `chart.off('event')` before binding to avoid duplicate executions or increased memory usage.
- Use lightweight operations (e.g., `dispatchAction`, `setOption`) inside event handlers to avoid blocking the rendering process.
- Validate against chart options and data queries to ensure that the fields handled in the event are consistent with the current data.
---
url: /guide/index.md
---
---
url: /solution/crm/guide/guide-overview.md
---
# System Overview & Dashboards
> This chapter covers the two main dashboards — Analytics (data analysis center) and Overview (daily workspace).
## System Overview
CRM 2.0 is a complete sales management system covering the entire process from lead acquisition to order delivery. After logging in, the top menu bar is your main navigation entry.
### Multi-language & Themes
The system supports language switching (top-right corner). All JS blocks and charts are adapted for multiple languages.
Both light and dark themes are supported, but we currently **recommend light theme + compact mode** for higher information density. Some display issues in dark mode will be fixed in future updates.

---
## Analytics — Data Analysis Center
Analytics is the first page in the menu bar and the first screen you see when opening the system.
### Global Filters
At the top of the page, there is a filter bar with **Date Range** and **Owner** filter conditions. After filtering, all charts on the page refresh in sync.

### KPI Cards
Below the filter bar are 4 KPI cards:
| Card | Description | Click Behavior |
|------|-------------|----------------|
| **Total Revenue** | Cumulative revenue amount | Popup: payment status pie chart + monthly revenue trend |
| **New Leads** | Number of new leads in the period | Redirect to Leads page, auto-filtered to "New" status |
| **Conversion Rate** | Lead-to-deal conversion ratio | Popup: stage distribution pie chart + amount bar chart |
| **Avg Deal Cycle** | Average days from creation to close | Popup: cycle distribution + monthly won trend |
Each card supports **click-through drill-down** — popups show more detailed analysis charts. With customization capability, you can drill further (company → department → individual).

:::tip[Data looks reduced after clicking?]
When you click a KPI card to jump to a list page, the URL carries filter parameters (e.g., `?status=new`). If you notice fewer records, it's because this parameter is still active. Navigate back to the dashboard and re-enter the list page to restore full data.
:::

### Charts Area
Below the KPIs are 5 core charts:
| Chart | Type | Description | Click Behavior |
|-------|------|-------------|----------------|
| **Opportunity Stage Distribution** | Bar chart | Count, amount, and weighted probability per stage | Popup: drill-through by customer/owner/month |
| **Sales Funnel** | Funnel | Lead → Opportunity → Quotation → Order conversion | Click to jump to corresponding entity page |
| **Monthly Sales Trend** | Bar + Line | Monthly revenue, order count, average order value | Jump to Orders page (with month parameter) |
| **Customer Growth Trend** | Bar + Line | Monthly new customers, cumulative total | Jump to Customers page |
| **Industry Distribution** | Pie chart | Customer distribution by industry | Jump to Customers page |

#### Sales Funnel
Shows the conversion rate across the full pipeline: Lead → Opportunity → Quotation → Order. Each layer is clickable, redirecting to the corresponding entity list page (e.g., clicking the Opportunity layer → jumps to the opportunity list).
#### Monthly Sales Trend
Bar chart shows monthly revenue, with line overlays for order count and average order value. Clicking a month's bar → jumps to the Orders page with that month's time filter pre-applied (e.g., `?month=2026-02`), showing that month's order details directly.
#### Customer Growth Trend
Bar chart shows monthly new customer count, line shows cumulative total. Clicking a month's bar → jumps to Customers page filtered to that month's new customers.
#### Industry Distribution
Pie chart shows customer distribution by industry with associated order amounts. Clicking an industry segment → jumps to Customers page filtered to that industry.
### Opportunity Stage Drill-through
Clicking a stage bar in the Opportunity Stage Distribution chart opens a deep analysis popup for that stage:
- **Monthly trend**: Monthly changes for opportunities in this stage
- **By owner**: Who is working on these opportunities
- **By customer**: Which customers have opportunities in this stage
- **Bottom summary**: Select customers to view cumulative amounts

Each stage (Prospecting / Analysis / Proposal / Negotiation / Won / Lost) has different drill-through content, reflecting the focus areas of each stage.
The core question this chart answers: **Where in the funnel is the most drop-off?** If the Proposal stage has many opportunities piling up but few moving to Negotiation, it suggests a problem in the quotation process.

### Chart Configuration (Advanced)
Each chart has three configuration dimensions:
1. **SQL Data Source**: Determines what data the chart displays; you can verify queries in the SQL builder
2. **Chart Style**: JSON configuration in the customization area, controlling chart appearance
3. **Events**: Click behavior (popup OpenView / page redirect)

### Filter Linkage
When any filter condition in the top filter bar is changed, **all charts on the page refresh simultaneously** — no need to set filters individually. Typical usage:
- **View someone's performance**: Select an Owner → all page data switches to that person's leads, opportunities, and orders
- **Compare time periods**: Switch date range from "This Month" to "This Quarter" → trend chart ranges update in sync
The linkage between filter bar and charts is implemented through **page event flows** — form variables are injected before rendering, and charts reference filter values through variables in their SQL.


:::note
SQL templates currently only support `if` syntax for conditional logic. We recommend referencing existing templates in the system, or using AI to assist with modifications.
:::
---
## Overview — Daily Workspace
Overview is the second dashboard page, focused on daily operations rather than data analysis. The core question it answers: **What should I do today? Which leads are worth following up on?**

### Top Leads
Automatically filters leads with AI score ≥ 75 and status New / Working (Top 5), showing for each:
- **AI Score Gauge**: Circular gauge visually showing lead quality (green = high score = worth prioritizing)
- **AI Recommended Next Step**: System auto-recommends follow-up actions based on lead characteristics (e.g., "Schedule a demo")
- **Lead Basic Info**: Name, company, source, creation time
Click a lead name to jump to its details, or click "View All" to go to the leads list page. A quick glance at this table each morning tells you who to contact first.

### Today's Tasks
A list of today's activities (meetings, calls, tasks, etc.), supporting:
- **One-click complete**: Click "Done" to mark a task complete; it turns gray
- **Overdue reminder**: Unfinished overdue tasks are highlighted in red
- **View details**: Click the task name to open details
- **Create new**: Add a new activity record directly here

### Activity Calendar
FullCalendar view with activities color-coded by type (meetings/calls/tasks/emails/notes). Supports month/week/day switching, drag-to-reschedule, and click-to-view details.

---
## Other Dashboards (More Charts)
Three additional dashboards for different roles are available in the menu. They are provided as references and can be hidden as needed:
| Dashboard | Target User | Features |
|-----------|-------------|----------|
| **Sales Manager** | Sales team leads | Team leaderboard, risk scatter plot, monthly targets |
| **Sales Rep** | Individual reps | Data auto-filtered by current user; shows only your own performance |
| **Executive** | VP Sales / C-Suite | Revenue forecast, customer health, Win/Loss trends |

Dashboards you don't need can be hidden from the menu without affecting system functionality.

---
## KPI Drill-through
You may have noticed that almost every number and chart above is "clickable." This is the core interaction pattern in the CRM — **KPI drill-through**: click a summary number → see the detailed data behind it.
Drill-through comes in two forms:
| Form | Use Case | Example |
|------|----------|---------|
| **Popup drill-through** | Multi-dimensional analysis | Click "Total Revenue" → popup shows pie chart + trends |
| **Page redirect** | View and operate on detail records | Click "New Leads" → jump to Leads list |
**Example**: In the Analytics "Monthly Sales Trend" chart, you notice February's revenue bar is notably low → click that bar → the system jumps to the Orders page with `month = 2026-02` pre-applied → you immediately see all February order details to investigate further.
> The dashboard isn't just for viewing — it's the system's navigation hub. Every number is an entry point, guiding you from macro to micro, layer by layer to the root cause.
---
After understanding the system layout and dashboards, go back to the [User Guide](./) for subsequent chapters.
## Related Pages
- [CRM User Guide](./)
- [Solution Overview](../index)
- [Installation](../installation)
---
url: /solution/crm/guide/index.md
---
# User Guide
> This guide walks you through the core CRM sales process from start to finish. We recommend reading in order — each chapter builds on the previous one.
## Reading Order
The core business flow is **Lead → Customer/Contact → Opportunity → Quotation → Order**. This guide follows that path:
| Chapter | Content | What You'll Learn |
|---------|---------|-------------------|
| [System Overview & Dashboards](./guide-overview) | System overview, Analytics dashboard, Overview workspace | Menu structure, KPI drill-through, chart linkage |
| Lead Management | Creating, filtering, AI scoring, follow-up, and conversion | How to efficiently identify high-quality leads and convert them to customers + opportunities |
| Opportunities & Quotations | Kanban board, stage progression, quotation creation, approval workflow | How to move deals through the sales funnel and complete the approval process |
| Order Management | Quotation to order, status tracking, payment monitoring | How to close the loop from quotation to delivery |
| Customer Management | Customer 360 view, health scores, customer merge | How to manage the full customer lifecycle |
| AI Employees | AI button usage, scenario examples | How to use AI to assist daily sales work |
| Emails & Contacts | Email send/receive, contact management, CRM linking | How to handle customer communication within the CRM |
## Prerequisites
- CRM 2.0 installed and running (see [Installation](../installation))
- Logged in with an admin account
## System Navigation
After logging in, the top menu bar is your main navigation:
```
📁 Dashboards
Analytics
Overview
📁 More Charts
Executive
Sales Rep
Sales Manager
Leads
Customers
Opportunities
Products
Orders
📁 Settings
Contacts
Activity
Exchange Rate
Stage Settings
Emails
Data Analysis
```
**Dashboards → Analytics** is the first page you see every day — it summarizes core KPIs, sales funnel, and trend charts to give you a quick overview of business performance.
Ready? Let's start with [System Overview & Dashboards](./guide-overview).
---
url: /data-sources/development/index.md
---
# Data Source Extension
:::tip
Content to be added
:::
---
url: /data-sources/file-manager/development/index.md
---
# Extension Development
## Extending Frontend File Types
For uploaded files, the client UI can display different previews based on file types. The attachment field of the file manager uses a built-in browser-based (iframe) file preview, supporting most file types (such as images, videos, audio, and PDFs) for direct preview in the browser. When a file type is not supported for browser preview or requires special interaction, additional preview components can be extended based on the file type.
### Example
For example, if you want to extend a carousel component for image files, you can use the following code:
```ts
import match from 'mime-match';
import { Plugin, attachmentFileTypes } from '@nocobase/client';
class MyPlugin extends Plugin {
load() {
attachmentFileTypes.add({
match(file) {
return match(file.mimetype, 'image/*');
},
Previewer({ index, list, onSwitchIndex }) {
const onDownload = useCallback(
(e) => {
e.preventDefault();
const file = list[index];
saveAs(file.url, `${file.title}${file.extname}`);
},
[index, list],
);
return (
onSwitchIndex(null)}
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
imageTitle={list[index]?.title}
toolbarButtons={[
,
]}
/>
);
},
});
}
}
```
The `attachmentFileTypes` is an entry object provided by the `@nocobase/client` package for extending file types. You can use its `add` method to extend a file type descriptor.
Each file type must implement a `match()` method to check if the file type meets the requirements. In the example, the `mime-match` package is used to check the file's `mimetype` attribute. If it matches `image/*`, it is considered a file type that needs processing. If it does not match, it will fall back to the built-in type.
The `Previewer` property on the type descriptor is the component used for previewing. When the file type matches, this component will be rendered for preview. It is generally recommended to use a modal component (like ``) as the base container and place the preview and interactive content within that component to implement the preview functionality.
### API
```ts
export interface FileModel {
id: number;
filename: string;
path: string;
title: string;
url: string;
extname: string;
size: number;
mimetype: string;
}
export interface PreviewerProps {
index: number;
list: FileModel[];
onSwitchIndex(index): void;
}
export interface AttachmentFileType {
match(file: any): boolean;
Previewer?: React.ComponentType;
}
export class AttachmentFileTypes {
add(type: AttachmentFileType): void;
}
```
#### `attachmentFileTypes`
`attachmentFileTypes` is a global instance imported from the `@nocobase/client` package:
```ts
import { attachmentFileTypes } from '@nocobase/client';
```
#### `attachmentFileTypes.add()`
Registers a new file type descriptor with the file type registry. The descriptor's type is `AttachmentFileType`.
#### `AttachmentFileType`
##### `match()`
A method for matching file formats.
The `file` parameter is a data object for the uploaded file, containing properties that can be used for type checking:
* `mimetype`: The file's mimetype.
* `extname`: The file extension, including the `.`.
* `path`: The relative storage path of the file.
* `url`: The file's URL.
Returns a `boolean` indicating whether the file is a match.
##### `Previewer`
A React component for previewing the file.
Props:
* `index`: The index of the file in the attachment list.
* `list`: The list of attachments.
* `onSwitchIndex`: A function to switch the previewed file by its index.
The `onSwitchIndex` function can be called with any index from the `list` to switch to another file. Calling it with `null` closes the preview component.
```ts
onSwitchIndex(null);
```
---
url: /development/index.md
---
---
url: /file-manager/development/index.md
---
# Extension Development
## Extending Storage Engines
### Server-side
1. **Inherit `StorageType`**
Create a new class and implement the `make()` and `delete()` methods, and override hooks like `getFileURL()`, `getFileStream()`, and `getFileData()` if necessary.
Example:
```ts
// packages/my-plugin/src/server/storages/custom.ts
import { AttachmentModel, StorageModel, StorageType } from '@nocobase/plugin-file-manager';
import type { StorageEngine } from 'multer';
import multer from 'multer';
import path from 'path';
import fs from 'fs/promises';
export class CustomStorageType extends StorageType {
make(): StorageEngine {
return multer.diskStorage({
destination: path.resolve('custom-uploads'),
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
});
}
async delete(records: AttachmentModel[]) {
let deleted = 0;
const failures: AttachmentModel[] = [];
for (const record of records) {
try {
await fs.unlink(path.resolve('custom-uploads', record.path || '', record.filename));
deleted += 1;
} catch (error) {
failures.push(record);
}
}
return [deleted, failures];
}
}
```
4. **Register the new type**
Inject the new storage implementation in the plugin's `beforeLoad` or `load` lifecycle:
```ts
// packages/my-plugin/src/server/plugin.ts
import { Plugin } from '@nocobase/server';
import PluginFileManagerServer from '@nocobase/plugin-file-manager';
import { CustomStorageType } from './storages/custom';
export default class MyStoragePluginServer extends Plugin {
async load() {
const fileManager = this.app.pm.get(PluginFileManagerServer);
fileManager.registerStorageType('custom-storage', CustomStorageType);
}
}
```
After registration, the storage configuration will appear in the `storages` resource just like built-in types. The configuration provided by `StorageType.defaults()` can be used to auto-fill forms or initialize default records.
## Extending Frontend File Types
For uploaded files, different preview content can be displayed on the frontend interface based on different file types. The file manager's attachment field has a built-in browser-based file preview (embedded in an iframe), which supports previewing most file formats (such as images, videos, audio, and PDFs) directly in the browser. When a file format is not supported by the browser for preview, or when special preview interactions are required, you can extend the file type-based preview component.
### Example
For example, if you want to integrate a custom online preview for Office files, you can use the following code:
```tsx
import React, { useMemo } from 'react';
import { Plugin, matchMimetype } from '@nocobase/client';
import { filePreviewTypes } from '@nocobase/plugin-file-manager/client';
class MyPlugin extends Plugin {
load() {
filePreviewTypes.add({
match(file) {
return matchMimetype(file, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
},
Previewer({ file }) {
const url = useMemo(() => {
const src =
file.url.startsWith('https://') || file.url.startsWith('http://')
? file.url
: `${location.origin}/${file.url.replace(/^\//, '')}`;
const u = new URL('https://view.officeapps.live.com/op/embed.aspx');
u.searchParams.set('src', src);
return u.href;
}, [file.url]);
return ;
},
});
}
}
```
Here, `filePreviewTypes` is the entry object provided by `@nocobase/plugin-file-manager/client` for extending file previews. Use its `add` method to extend a file type descriptor object.
Each file type must implement a `match()` method to check whether the file type meets the requirements. In the example, `matchMimetype` is used to check the file's `mimetype` attribute. If it matches the `docx` type, it is considered the file type to be handled. If it does not match, it will fall back to built-in type handling.
The `Previewer` property on the type descriptor object is the component used for previewing. When the file type matches, this component will be rendered in the file preview modal. You can return any React view (such as an iframe, player, or chart).
### API
```ts
export interface FilePreviewerProps {
file: any;
index: number;
list: any[];
}
export interface FilePreviewType {
match(file: any): boolean;
getThumbnailURL?: (file: any) => string | null;
Previewer?: React.ComponentType;
}
export class FilePreviewTypes {
add(type: FilePreviewType): void;
}
```
#### `filePreviewTypes`
`filePreviewTypes` is a global instance, imported from `@nocobase/plugin-file-manager/client`:
```ts
import { filePreviewTypes } from '@nocobase/plugin-file-manager/client';
```
#### `filePreviewTypes.add()`
Registers a new file type descriptor object with the file type registry. The type of the descriptor object is `FilePreviewType`.
#### `FilePreviewType`
##### `match()`
File format matching method.
The input parameter `file` is the data object of the uploaded file, containing relevant properties that can be used for type checking:
* `mimetype`: mimetype description
* `extname`: file extension, including "."
* `path`: relative storage path of the file
* `url`: file URL
Returns a `boolean` value indicating the matching result.
##### `getThumbnailURL`
Used to return the thumbnail URL in the file list. If the return value is empty, the built-in placeholder image will be used.
##### `Previewer`
A React component for previewing files.
The incoming Props are:
* `file`: The current file object (may be a string URL or an object containing `url`/`preview`)
* `index`: Index of the file in the list
* `list`: File list
---
url: /notification-manager/development/api.md
---
# API Reference
## Server Side
### `BaseNotificationChannel`
This abstract class represents a base for different types of notification channels, defining essential interfaces for channel implementation. To add a new notification channel, you must extend this class and implement its methods.
```ts
export abstract class BaseNotificationChannel {
constructor(protected app: Application) {}
abstract send(params: {
channel: ChannelOptions;
message: Message;
}): Promise<{
message: Message;
status: 'success' | 'fail';
reason?: string;
}>;
}
```
### `PluginNotificationManagerServer`
This server-side plugin serves as a notification management tool, providing methods for registering notification channel types and sending notifications.
#### `registerChannelType()`
This method registers a new channel type on the server side. Example usage is provided below.
```ts
import PluginNotificationManagerServer from '@nocobase/plugin-notification-manager';
import { Plugin } from '@nocobase/server';
import { ExampleServer } from './example-server';
export class PluginNotificationExampleServer extends Plugin {
async load() {
const notificationServer = this.pm.get(
PluginNotificationManagerServer,
) as PluginNotificationManagerServer;
notificationServer.registerChannelType({
type: 'example-sms',
Channel: ExampleServer,
});
}
}
export default PluginNotificationExampleServer;
```
##### Signature
`registerChannelType({ type, Channel }: {type: string, Channel: BaseNotificationChannel })`
#### `send()`
The `send` method is used to dispatch notifications via a specified channel.
```ts
// In-app message
send({
channelName: 'in-app-message',
message: {
title: 'In-app message test title',
content: 'In-app message test'
},
receivers: {
type: 'userId',
value: [1, 2, 3]
},
triggerFrom: 'workflow'
});
// Email
send({
channelName: 'email',
message: {
title: 'Email test title',
content: 'Email test'
},
receivers: {
type: 'channel-self-defined',
channelType: 'email',
value: ['a@example.com', 'b@example.com']
},
triggerFrom: 'workflow'
});
```
##### Signature
`send(sendConfig: {channelName: String, message: Object, receivers: ReceiversType, triggerFrom: String })`
The `receivers` field currently supports two formats: NocoBase user IDs `userId` or custom channel configurations `channel-self-defined`.
```ts
type ReceiversType =
| { value: number[]; type: 'userId' }
| { value: any; type: 'channel-self-defined'; channelType: string };
```
##### Detailed Information
`sendConfig`
| Property | Type | Description |
| ------------- | --------------- | ------------------ |
| `channelName` | `string` | Channel identifier |
| `message` | `object` | Message object |
| `receivers` | `ReceiversType` | Recipients |
| `triggerFrom` | `string` | Source of trigger |
## Client Side
### `PluginNotificationManagerClient`
#### `channelTypes`
The library of registered channel types.
##### Signature
`channelTypes: Registry`
#### `registerChannelType()`
Registers a client-side channel type.
##### Signature
`registerChannelType(params: registerTypeOptions)`
##### Type
```ts
type registerTypeOptions = {
title: string; // Display title for the channel
type: string; // Channel identifier
components: {
ChannelConfigForm?: ComponentType; // Channel configuration form component;
MessageConfigForm?: ComponentType<{ variableOptions: any }>; // Message configuration form component;
ContentConfigForm?: ComponentType<{ variableOptions: any }>; // Content configuration form component (for message content only, excluding recipient configuration);
};
meta?: {
// Metadata for channel configuration
createable?: boolean; // Whether new channels can be added;
editable?: boolean; // Whether channel configuration is editable;
deletable?: boolean; // Whether channel configuration is deletable;
};
};
type RegisterChannelType = (params: ChannelType) => void;
```
---
url: /notification-manager/development/extension.md
---
# Extending Notification Channel Types
NocoBase supports extending notification channel types on demand, such as for SMS notifications and app push notifications.
## Client
### Channel Type Registration
The client channel configuration and message configuration interface are registered through the `registerChannelType` method provided by the notification management plugin client:
```ts
import PluginNotificationManagerClient from '@nocobase/plugin-notification-manager/client';
class PluginNotificationExampleClient extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {
const notification = this.pm.get(PluginNotificationManagerClient);
notification.registerChannelType({
title: 'Example SMS', // Channel type name
type: 'example-sms', // Channel type identifier
components: {
ChannelConfigForm, // Channel configuration form
MessageConfigForm, // Message configuration form
},
});
}
}
export default PluginNotificationExampleClient;
```
## Server
### Extending Abstract Class
The core of server development involves extending the `BaseNotificationChannel` abstract class and implementing the `send` method, which contains the business logic for sending notifications through the extended plugin.
```ts
import { BaseNotificationChannel } from '@nocobase/plugin-notification-manager';
export class ExampleServer extends BaseNotificationChannel {
async send(args): Promise {
console.log('ExampleServer send', args);
return { status: 'success', message: args.message };
}
}
```
### Server Registration
The `registerChannelType` method of the notification server core should be called to register the server implementation class in the core:
```ts
import PluginNotificationManagerServer from '@nocobase/plugin-notification-manager';
import { Plugin } from '@nocobase/server';
import { ExampleServer } from './example-server';
export class PluginNotificationExampleServer extends Plugin {
async load() {
const notificationServer = this.pm.get(
PluginNotificationManagerServer,
) as PluginNotificationManagerServer;
notificationServer.registerChannelType({
type: 'example-sms',
Channel: ExampleServer,
});
}
}
export default PluginNotificationExampleServer;
```
## Full Example
Here is a sample notification extension to describe in detail how to develop an extension.
Suppose we want to add SMS notification to NocoBase using a platform's SMS gateway.
### Plugin Creation
1. Run the command to create the plugin `yarn pm add @nocobase/plugin-notification-example`
### Client Development
For the client, develop two form components: `ChannelConfigForm` (Channel Configuration Form) and `MessageConfigForm` (Message Configuration Form).
#### ChannelConfigForm
To send SMS messages, an API key and secret are required. Create a new file named `ChannelConfigForm.tsx` in the `src/client` directory:
```ts
import React from 'react';
import { SchemaComponent } from '@nocobase/client';
import useLocalTranslation from './useLocalTranslation';
const ChannelConfigForm = () => {
const t = useLocalTranslation();
return (
);
};
export default ChannelConfigForm;
```
#### MessageConfigForm
The message configuration form mainly includes the configuration for recipients (`receivers`) and message content (`content`). Create a new file named `MessageConfigForm.tsx` in the `src/client` directory. The component receives `variableOptions` as a variable parameter. The content form is configured in the workflow node and typically needs to consume workflow node variables. The specific file content is as follows:
```ts
import React from 'react';
import { SchemaComponent } from '@nocobase/client';
import useLocalTranslation from './useLocalTranslation';
const MessageConfigForm = ({ variableOptions }) => {
const { t } = useLocalTranslation();
return (
);
};
export default MessageConfigForm
```
#### Client Component Registration
After developing the form configuration components, register them in the notification management core. Assume the platform name is "Example." Edit `src/client/index.tsx` as follows:
```ts
import { Plugin } from '@nocobase/client';
import PluginNotificationManagerClient from '@nocobase/plugin-notification-manager/client';
import { tval } from '@nocobase/utils/client';
import ChannelConfigForm from './ChannelConfigForm';
import MessageConfigForm from './MessageConfigForm';
class PluginNotificationExampleClient extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {
const notification = this.pm.get(PluginNotificationManagerClient);
notification.registerChannelType({
title: tval('Example SMS', { ns: '@nocobase/plugin-notification-example' }),
type: 'example-sms',
components: {
ChannelConfigForm,
MessageConfigForm,
},
});
}
}
export default PluginNotificationExampleClient;
```
At this point, the development of the client is complete.
### Server Development
The core of server development involves extending the `BaseNotificationChannel` abstract class and implementing the `send` method. The `send` method contains the business logic for the extension plugin to send notifications. Since this is an example, we will simply print the received arguments. In the `src/server` directory, add a file named `example-server.ts`:
```ts
import { BaseNotificationChannel } from '@nocobase/plugin-notification-manager';
export class ExampleServer extends BaseNotificationChannel {
async send(args): Promise {
console.log('ExampleServer send', args);
return { status: 'success', message: args.message };
}
}
```
Next, register the server extension plugin by editing `src/server/plugin.ts`:
```ts
import PluginNotificationManagerServer from '@nocobase/plugin-notification-manager';
import { Plugin } from '@nocobase/server';
import { ExampleServer } from './example-server';
export class PluginNotificationExampleServer extends Plugin {
async load() {
const notificationServer = this.pm.get(
PluginNotificationManagerServer,
) as PluginNotificationManagerServer;
notificationServer.registerChannelType({
type: 'example-sms',
Channel: ExampleServer,
});
}
}
export default PluginNotificationExampleServer;
```
### Plugin Registration and Launch
1. Run the registration command: `yarn pm add @nocobase/plugin-notification-example`
2. Run the enable command: `yarn pm enable @nocobase/plugin-notification-example`
### Channel Configuration
Upon visiting the Notification management channel page, you can see that the `Example SMS` channel has been enabled.

Add a sample channel.

Create a new workflow and configure the notification node.

Trigger the workflow execution to view the following information output in the console.

---
url: /workflow/development/api.md
---
# API Reference
## Server-side
The APIs available in the server-side package structure are shown in the following code:
```ts
import PluginWorkflowServer, {
Trigger,
Instruction,
EXECUTION_STATUS,
JOB_STATUS,
} from '@nocobase/plugin-workflow';
```
### `PluginWorkflowServer`
Workflow plugin class.
Usually, during the application's runtime, you can call `app.pm.get(PluginWorkflowServer)` anywhere you can get the application instance `app` to obtain the workflow plugin instance (referred to as `plugin` below).
#### `registerTrigger()`
Extends and registers a new trigger type.
**Signature**
`registerTrigger(type: string, trigger: typeof Trigger | Trigger })`
**Parameters**
| Parameter | Type | Description |
| --------- | --------------------------- | ---------------- |
| `type` | `string` | Trigger type identifier |
| `trigger` | `typeof Trigger \| Trigger` | Trigger type or instance |
**Example**
```ts
import PluginWorkflowServer, { Trigger } from '@nocobase/plugin-workflow';
function handler(this: MyTrigger, workflow: WorkflowModel, message: string) {
// trigger workflow
this.workflow.trigger(workflow, { data: message.data });
}
class MyTrigger extends Trigger {
messageHandlers: Map = new Map();
on(workflow: WorkflowModel) {
const messageHandler = handler.bind(this, workflow);
// listen some event to trigger workflow
process.on(
'message',
this.messageHandlers.set(workflow.id, messageHandler),
);
}
off(workflow: WorkflowModel) {
const messageHandler = this.messageHandlers.get(workflow.id);
// remove listener
process.off('message', messageHandler);
}
}
export default class MyPlugin extends Plugin {
load() {
// get workflow plugin instance
const workflowPlugin =
this.app.pm.get(PluginWorkflowServer);
// register trigger
workflowPlugin.registerTrigger('myTrigger', MyTrigger);
}
}
```
#### `registerInstruction()`
Extends and registers a new node type.
**Signature**
`registerInstruction(type: string, instruction: typeof Instruction | Instruction })`
**Parameters**
| Parameter | Type | Description |
| ------------- | ----------------------------------- | -------------- |
| `type` | `string` | Instruction type identifier |
| `instruction` | `typeof Instruction \| Instruction` | Instruction type or instance |
**Example**
```ts
import PluginWorkflowServer, { Instruction, JOB_STATUS } from '@nocobase/plugin-workflow';
class LogInstruction extends Instruction {
run(node, input, processor) {
console.log('my instruction runs!');
return {
status: JOB_STATUS.RESOVLED,
};
},
};
export default class MyPlugin extends Plugin {
load() {
// get workflow plugin instance
const workflowPlugin = this.app.pm.get(PluginWorkflowServer);
// register instruction
workflowPlugin.registerInstruction('log', LogInstruction);
}
}
```
#### `trigger()`
Triggers a specific workflow. Mainly used in custom triggers to trigger the corresponding workflow when a specific custom event is listened to.
**Signature**
`trigger(workflow: Workflow, context: any)`
**Parameters**
| Parameter | Type | Description |
| --- | --- | --- |
| `workflow` | `WorkflowModel` | The workflow object to be triggered |
| `context` | `object` | Context data provided at trigger time |
:::info{title=Tip}
`context` is currently a required item. If not provided, the workflow will not be triggered.
:::
**Example**
```ts
import { Trigger } from '@nocobase/plugin-workflow';
class MyTrigger extends Trigger {
timer: NodeJS.Timeout;
on(workflow) {
// register event
this.timer = setInterval(() => {
// trigger workflow
this.plugin.trigger(workflow, { date: new Date() });
}, workflow.config.interval ?? 60000);
}
}
```
#### `resume()`
Resumes a waiting workflow with a specific node job.
- Only workflows in the waiting state (`EXECUTION_STATUS.STARTED`) can be resumed.
- Only node jobs in the pending state (`JOB_STATUS.PENDING`) can be resumed.
**Signature**
`resume(job: JobModel)`
**Parameters**
| Parameter | Type | Description |
| ----- | ---------- | ---------------- |
| `job` | `JobModel` | The updated job object |
:::info{title=Tip}
The passed job object is generally an updated object, and its `status` is usually updated to a value other than `JOB_STATUS.PENDING`, otherwise it will continue to wait.
:::
**Example**
See [source code](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-workflow-manual/src/server/actions.ts#L99) for details.
### `Trigger`
The base class for triggers, used to extend custom trigger types.
| Parameter | Type | Description |
| ------------- | ----------------------------------------------------------- | ---------------------- |
| `constructor` | `(public readonly workflow: PluginWorkflowServer): Trigger` | Constructor |
| `on?` | `(workflow: WorkflowModel): void` | Event handler after enabling a workflow |
| `off?` | `(workflow: WorkflowModel): void` | Event handler after disabling a workflow |
`on`/`off` are used to register/unregister event listeners when a workflow is enabled/disabled. The passed parameter is the workflow instance corresponding to the trigger, which can be processed according to the configuration. Some trigger types that already have globally listened events may not need to implement these two methods. For example, in a scheduled trigger, you can register a timer in `on` and unregister it in `off`.
### `Instruction`
The base class for instruction types, used to extend custom instruction types.
| Parameter | Type | Description |
| ------------- | --------------------------------------------------------------- | ---------------------------------- |
| `constructor` | `(public readonly workflow: PluginWorkflowServer): Instruction` | Constructor |
| `run` | `Runner` | Execution logic for the first entry into the node |
| `resume?` | `Runner` | Execution logic for entering the node after resuming from an interruption |
| `getScope?` | `(node: FlowNodeModel, data: any, processor: Processor): any` | Provides the local variable content for the branch generated by the corresponding node |
**Related Types**
```ts
export type Job =
| {
status: JOB_STATUS[keyof JOB_STATUS];
result?: unknown;
[key: string]: unknown;
}
| JobModel
| null;
export type InstructionResult = Job | Promise;
export type Runner = (
node: FlowNodeModel,
input: JobModel,
processor: Processor,
) => InstructionResult;
export class Instruction {
run: Runner;
resume?: Runner;
}
```
For `getScope`, you can refer to the [implementation of the loop node](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-workflow-loop/src/server/LoopInstruction.ts#L83), which is used to provide local variable content for branches.
### `EXECUTION_STATUS`
A constant table for workflow execution plan statuses, used to identify the current status of the corresponding execution plan.
| Constant Name | Meaning |
| ------------------------------- | -------------------- |
| `EXECUTION_STATUS.QUEUEING` | Queueing |
| `EXECUTION_STATUS.STARTED` | Started |
| `EXECUTION_STATUS.RESOLVED` | Resolved |
| `EXECUTION_STATUS.FAILED` | Failed |
| `EXECUTION_STATUS.ERROR` | Error |
| `EXECUTION_STATUS.ABORTED` | Aborted |
| `EXECUTION_STATUS.CANCELED` | Canceled |
| `EXECUTION_STATUS.REJECTED` | Rejected |
| `EXECUTION_STATUS.RETRY_NEEDED` | Not successfully executed, retry needed |
Except for the first three, all others represent a failed state, but can be used to describe different reasons for failure.
### `JOB_STATUS`
A constant table for workflow node job statuses, used to identify the current status of the corresponding node job. The status generated by the node also affects the status of the entire execution plan.
| Constant Name | Meaning |
| ------------------------- | ---------------------------------------- |
| `JOB_STATUS.PENDING` | Pending: Execution has reached this node, but the instruction requires it to suspend and wait |
| `JOB_STATUS.RESOLVED` | Resolved |
| `JOB_STATUS.FAILED` | Failed: The execution of this node failed to meet the configured conditions |
| `JOB_STATUS.ERROR` | Error: An unhandled error occurred during the execution of this node |
| `JOB_STATUS.ABORTED` | Aborted: The execution of this node was terminated by other logic after being in a pending state |
| `JOB_STATUS.CANCELED` | Canceled: The execution of this node was manually canceled after being in a pending state |
| `JOB_STATUS.REJECTED` | Rejected: The continuation of this node was manually rejected after being in a pending state |
| `JOB_STATUS.RETRY_NEEDED` | Not successfully executed, retry needed |
## Client-side
The APIs available in the client-side package structure are shown in the following code:
```ts
import PluginWorkflowClient, {
Trigger,
Instruction,
} from '@nocobase/plugin-workflow/client';
```
### `PluginWorkflowClient`
#### `registerTrigger()`
Registers the configuration panel for the trigger type.
**Signature**
`registerTrigger(type: string, trigger: typeof Trigger | Trigger): void`
**Parameters**
| Parameter | Type | Description |
| --------- | --------------------------- | ------------------------------------ |
| `type` | `string` | Trigger type identifier, consistent with the identifier used for registration |
| `trigger` | `typeof Trigger \| Trigger` | Trigger type or instance |
#### `registerInstruction()`
Registers the configuration panel for the node type.
**Signature**
`registerInstruction(type: string, instruction: typeof Instruction | Instruction): void`
**Parameters**
| Parameter | Type | Description |
| ------------- | ----------------------------------- | ---------------------------------- |
| `type` | `string` | Node type identifier, consistent with the identifier used for registration |
| `instruction` | `typeof Instruction \| Instruction` | Node type or instance |
#### `registerInstructionGroup()`
Registers a node type group. NocoBase provides 4 default node type groups:
* `'control'`: Control
* `'collection'`: Collection operations
* `'manual'`: Manual processing
* `'extended'`: Other extensions
If you need to extend other groups, you can use this method to register them.
**Signature**
`registerInstructionGroup(type: string, group: { label: string }): void`
**Parameters**
| Parameter | Type | Description |
| --------- | ----------------- | ----------------------------- |
| `type` | `string` | Node group identifier, consistent with the identifier used for registration |
| `group` | `{ label: string }` | Group information, currently only includes the title |
**Example**
```js
export default class YourPluginClient extends Plugin {
load() {
const pluginWorkflow = this.app.pm.get(PluginWorkflowClient);
pluginWorkflow.registerInstructionGroup('ai', { label: `{{t("AI", { ns: "${NAMESPACE}" })}}` });
}
}
```
### `Trigger`
The base class for triggers, used to extend custom trigger types.
| Parameter | Type | Description |
| --------------- | ---------------------------------------------------------------- | ---------------------------------- |
| `title` | `string` | Trigger type name |
| `fieldset` | `{ [key: string]: ISchema }` | Collection of trigger configuration items |
| `scope?` | `{ [key: string]: any }` | Collection of objects that may be used in the configuration item Schema |
| `components?` | `{ [key: string]: React.FC }` | Collection of components that may be used in the configuration item Schema |
| `useVariables?` | `(config: any, options: UseVariableOptions ) => VariableOptions` | Value accessor for trigger context data |
- If `useVariables` is not set, it means that this type of trigger does not provide a value retrieval function, and the trigger's context data cannot be selected in the workflow nodes.
### `Instruction`
The base class for instructions, used to extend custom node types.
| Parameter | Type | Description |
| -------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `group` | `string` | Node type group identifier, currently available options: `'control'`/`'collection'`/`'manual'`/`'extended'` |
| `fieldset` | `Record` | Collection of node configuration items |
| `scope?` | `Record` | Collection of objects that may be used in the configuration item Schema |
| `components?` | `Record` | Collection of components that may be used in the configuration item Schema |
| `Component?` | `React.FC` | Custom rendering component for the node |
| `useVariables?` | `(node, options: UseVariableOptions) => VariableOption` | Method for the node to provide node variable options |
| `useScopeVariables?` | `(node, options?) => VariableOptions` | Method for the node to provide branch local variable options |
| `useInitializers?` | `(node) => SchemaInitializerItemType` | Method for the node to provide initializer options |
| `isAvailable?` | `(ctx: NodeAvailableContext) => boolean` | Method to determine if the node is available |
**Related Types**
```ts
export type NodeAvailableContext = {
workflow: object;
upstream: object;
branchIndex: number;
};
```
- If `useVariables` is not set, it means that this node type does not provide a value retrieval function, and the result data of this type of node cannot be selected in the workflow nodes. If the result value is singular (not selectable), you can return static content that expresses the corresponding information (see: [calculation node source code](https://github.com/nocobase/nocobase/blob/main/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/calculation.tsx#L68)). If it needs to be selectable (e.g., a property of an Object), you can customize the corresponding selection component output (see: [create data node source code](https://github.com/nocobase/nocobase/blob/main/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx#L41)).
- `Component` is a custom rendering component for the node. When the default node rendering is not sufficient, it can be completely overridden for custom node view rendering. For example, if you need to provide more action buttons or other interactions for the start node of a branch type, you would use this method (see: [parallel branch source code](https://github.com/nocobase/nocobase/blob/main/packages/plugins/@nocobase/plugin-workflow-parallel/src/client/ParallelInstruction.tsx)).
- `useInitializers` is used to provide a method for initializing blocks. For example, in a manual node, you can initialize related user blocks based on upstream nodes. If this method is provided, it will be available when initializing blocks in the manual node's interface configuration (see: [create data node source code](https://github.com/nocobase/nocobase/blob/main/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/create.tsx#L71)).
- `isAvailable` is mainly used to determine whether a node can be used (added) in the current environment. The current environment includes the current workflow, upstream nodes, and the current branch index.
---
url: /workflow/development/index.md
---
# Overview
The built-in features of the Workflow cannot cover all scenarios. For instance, the built-in node types cannot exhaust every possible operation in all business scenarios. Therefore, we also provide a design for extending the Workflow, including extending trigger and node types. In business scenarios where the built-in features are insufficient, you can extend them using low-code methods.
Extensions are mainly divided into two parts:
- [Extend Trigger Types](./trigger.md)
- [Extend Node Types](./node.md)
## Other Content
- [API Reference](./api.md)
---
url: /workflow/development/node.md
---
# Extending Node Types
A node's type is essentially an operational instruction. Different instructions represent different operations executed in the workflow.
Similar to triggers, extending node types is also divided into two parts: server-side and client-side. The server-side needs to implement the logic for the registered instruction, while the client-side needs to provide the interface configuration for the parameters of the node where the instruction is located.
## Server-side
### The Simplest Node Instruction
The core content of an instruction is a function, meaning the `run` method in the instruction class must be implemented to execute the instruction's logic. Any necessary operations can be performed within the function, such as database operations, file operations, calling third-party APIs, etc.
All instructions need to be derived from the `Instruction` base class. The simplest instruction only needs to implement a `run` function:
```ts
import { Instruction, JOB_STATUS } from '@nocobase/plugin-workflow';
export class MyInstruction extends Instruction {
run(node, input, processor) {
console.log('my instruction runs!');
return {
status: JOB_STATUS.RESOVLED,
};
}
}
```
And register this instruction with the workflow plugin:
```ts
export default class MyPlugin extends Plugin {
load() {
// get workflow plugin instance
const workflowPlugin = this.app.getPlugin(WorkflowPlugin);
// register instruction
workflowPlugin.registerInstruction('my-instruction', MyInstruction);
}
}
```
The status value (`status`) in the instruction's return object is mandatory and must be a value from the `JOB_STATUS` constant. This value determines the flow of subsequent processing for this node in the workflow. Typically, `JOB_STATUS.RESOVLED` is used, indicating that the node has executed successfully and the execution will continue to the next nodes. If there is a result value that needs to be saved in advance, you can also call the `processor.saveJob` method and return its return object. The executor will generate an execution result record based on this object.
### Node Result Value
If there is a specific execution result, especially data prepared for use by subsequent nodes, it can be returned through the `result` property and saved in the node's job object:
```ts
import { Instruction, JOB_STATUS } from '@nocobase/plugin-workflow';
export class RandomStringInstruction extends Instruction {
run(node, input, processor) {
// customized config from node
const { digit = 1 } = node.config;
const result = `${Math.round(10 ** digit * Math.random())}`.padStart(
digit,
'0',
);
return {
status: JOB_STATUS.RESOVLED,
result,
};
},
};
```
Here, `node.config` is the node's configuration item, which can be any required value. It will be saved as a `JSON` type field in the corresponding node record in the database.
### Instruction Error Handling
If exceptions may occur during execution, you can catch them in advance and return a failed status:
```ts
import { JOB_STATUS } from '@nocobase/plugin-workflow';
export const errorInstruction = {
run(node, input, processor) {
try {
throw new Error('exception');
} catch (error) {
return {
status: JOB_STATUS.ERROR,
result: error,
};
}
},
};
```
If predictable exceptions are not caught, the workflow engine will automatically catch them and return an error status to prevent uncaught exceptions from crashing the program.
### Asynchronous Nodes
When flow control or asynchronous (time-consuming) I/O operations are needed, the `run` method can return an object with a `status` of `JOB_STATUS.PENDING`, prompting the executor to wait (suspend) until some external asynchronous operation is completed, and then notify the workflow engine to continue execution. If a pending status value is returned in the `run` function, the instruction must implement the `resume` method; otherwise, the workflow execution cannot be resumed:
```ts
import { Instruction, JOB_STATUS } from '@nocobase/plugin-workflow';
export class PayInstruction extends Instruction {
async run(node, input, processor) {
// job could be create first via processor
const job = await processor.saveJob({
status: JOB_STATUS.PENDING,
});
const { workflow } = processor;
// do payment asynchronously
paymentService.pay(node.config, (result) => {
// notify processor to resume the job
return workflow.resume(job.id, result);
});
// return created job instance
return job;
}
resume(node, job, processor) {
// check payment status
job.set('status', job.result.status === 'ok' ? JOB_STATUS.RESOVLED : JOB_STATUS.REJECTED);
return job;
},
};
```
Here, `paymentService` refers to a payment service. In the service's callback, the workflow is triggered to resume the execution of the corresponding job, and the current process exits first. Later, the workflow engine creates a new processor and passes it to the node's `resume` method to continue executing the previously suspended node.
:::info{title=Note}
The "asynchronous operation" mentioned here does not refer to `async` functions in JavaScript, but rather to non-instantaneous return operations when interacting with other external systems, such as a payment service that needs to wait for another notification to know the result.
:::
### Node Result Status
The execution status of a node affects the success or failure of the entire workflow. Typically, without branches, the failure of a node will directly cause the entire workflow to fail. The most common scenario is that if a node executes successfully, it proceeds to the next node in the node table until there are no more subsequent nodes, at which point the entire workflow completes with a successful status.
If a node returns a failed execution status during execution, the engine will handle it differently depending on the following two situations:
1. The node that returns a failed status is in the main workflow, meaning it is not within any branch workflow opened by an upstream node. In this case, the entire main workflow is judged as failed, and the process exits.
2. The node that returns a failed status is within a branch workflow. In this case, the responsibility for determining the next state of the workflow is handed over to the node that opened the branch. The internal logic of that node will decide the state of the subsequent workflow, and this decision will recursively propagate up to the main workflow.
Ultimately, the next state of the entire workflow is determined at the nodes of the main workflow. If a node in the main workflow returns a failure, the entire workflow ends with a failed status.
If any node returns a "pending" status after execution, the entire execution process will be temporarily interrupted and suspended, waiting for an event defined by the corresponding node to trigger the resumption of the workflow. For example, the Manual Node, when executed, will pause at that node with a "pending" status, waiting for manual intervention to decide whether to approve. If the manually entered status is approval, the subsequent workflow nodes will continue; otherwise, it will be handled according to the failure logic described earlier.
For more instruction return statuses, please refer to the Workflow API Reference section.
### Early Exit
In some special workflows, it may be necessary to end the workflow directly within a node. You can return `null` to indicate exiting the current workflow, and subsequent nodes will not be executed.
This situation is common in flow control type nodes, such as the Parallel Branch Node ([code reference](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-workflow-parallel/src/server/ParallelInstruction.ts#L87)), where the current node's workflow exits, but new workflows are started for each sub-branch and continue to execute.
:::warn{title=Note}
Scheduling branch workflows with extended nodes has a certain complexity and requires careful handling and thorough testing.
:::
### Learn More
For the definitions of various parameters for defining node types, see the Workflow API Reference section.
## Client-side
Similar to triggers, the configuration form for an instruction (node type) needs to be implemented on the client-side.
### The Simplest Node Instruction
All instructions need to be derived from the `Instruction` base class. The related properties and methods are used for configuring and using the node.
For example, if we need to provide a configuration interface for the random number string type (`randomString`) node defined on the server-side above, which has a configuration item `digit` representing the number of digits for the random number, we would use a number input box in the configuration form to receive user input.
```tsx pure
import WorkflowPlugin, { Instruction, VariableOption } from '@nocobase/workflow/client';
class MyInstruction extends Instruction {
title = 'Random number string';
type = 'randomString';
group = 'extended';
fieldset = {
'digit': {
type: 'number',
title: 'Digit',
name: 'digit',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
min: 1,
max: 10,
},
default: 6,
},
};
useVariables(node, options): VariableOption {
return {
value: node.key,
label: node.title,
};
}
}
export default class MyPlugin extends Plugin {
load() {
// get workflow plugin instance
const workflowPlugin = this.app.getPlugin(WorkflowPlugin);
// register instruction
workflowPlugin.registerInstruction('randomString', MyInstruction);
}
}
```
:::info{title=Note}
The node type identifier registered on the client-side must be consistent with the one on the server-side, otherwise it will cause errors.
:::
### Providing Node Results as Variables
You may notice the `useVariables` method in the example above. If you need to use the node's result (the `result` part) as a variable for subsequent nodes, you need to implement this method in the inherited instruction class and return an object that conforms to the `VariableOption` type. This object serves as a structural description of the node's execution result, providing variable name mapping for selection and use in subsequent nodes.
The `VariableOption` type is defined as follows:
```ts
export type VariableOption = {
value?: string;
label?: string;
children?: VariableOption[] | null;
[key: string]: any;
};
```
The core is the `value` property, which represents the segmented path value of the variable name. `label` is used for display on the interface, and `children` is used to represent a multi-level variable structure, which is used when the node's result is a deeply nested object.
A usable variable is represented internally in the system as a path template string separated by `.`, for example, `{{jobsMapByNodeKey.2dw92cdf.abc}}`. Here, `jobsMapByNodeKey` represents the result set of all nodes (internally defined, no need to handle), `2dw92cdf` is the node's `key`, and `abc` is a custom property in the node's result object.
Additionally, since a node's result can also be a simple value, when providing node variables, the first level **must** be the description of the node itself:
```ts
{
value: node.key,
label: node.title,
}
```
That is, the first level is the node's `key` and title. For example, in the calculation node's [code reference](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-workflow/src/client/nodes/calculation.tsx#L77), when using the result of the calculation node, the interface options are as follows:

When the node's result is a complex object, you can use `children` to continue describing nested properties. For example, a custom instruction might return the following JSON data:
```json
{
"message": "ok",
"data": {
"id": 1,
"name": "test",
}
}
```
Then you can return it through the `useVariables` method as follows:
```ts
useVariables(node, options): VariableOption {
return {
value: node.key,
label: node.title,
children: [
{
value: 'message',
label: 'Message',
},
{
value: 'data',
label: 'Data',
children: [
{
value: 'id',
label: 'ID',
},
{
value: 'name',
label: 'Name',
},
],
},
],
};
}
```
This way, in subsequent nodes, you can use the following interface to select variables from it:

:::info{title="Note"}
When a structure in the result is an array of deeply nested objects, you can also use `children` to describe the path, but it cannot include array indices. This is because in NocoBase workflow's variable handling, the variable path description for an array of objects is automatically flattened into an array of deep values when used, and you cannot access a specific value by its index.
:::
### Node Availability
By default, any node can be added to a workflow. However, in some cases, a node may not be applicable in certain types of workflows or branches. In such situations, you can configure the node's availability using `isAvailable`:
```ts
// Type definition
export abstract class Instruction {
isAvailable?(ctx: NodeAvailableContext): boolean;
}
export type NodeAvailableContext = {
// Workflow plugin instance
engine: WorkflowPlugin;
// Workflow instance
workflow: object;
// Upstream node
upstream: object;
// Whether it is a branch node (branch number)
branchIndex: number;
};
```
The `isAvailable` method returns `true` if the node is available, and `false` if it is not. The `ctx` parameter contains the context information of the current node, which can be used to determine its availability.
If there are no special requirements, you do not need to implement the `isAvailable` method, as nodes are available by default. The most common scenario requiring configuration is when a node might be a time-consuming operation and is not suitable for execution in a synchronous workflow. You can use the `isAvailable` method to restrict its use. For example:
```ts
isAvailable({ engine, workflow, upstream, branchIndex }) {
return !engine.isWorkflowSync(workflow);
}
```
### Learn More
For the definitions of various parameters for defining node types, see the Workflow API Reference section.
---
url: /workflow/development/trigger.md
---
# Extend Trigger Types
Every workflow must be configured with a specific trigger, which serves as the entry point for starting the process execution.
A trigger type usually represents a specific system environment event. During the application's runtime lifecycle, any part that provides subscribable events can be used to define a trigger type. For example, receiving requests, collection operations, scheduled tasks, etc.
Trigger types are registered in the plugin's trigger table based on a string identifier. The Workflow plugin has several built-in triggers:
- `'collection'`: Triggered by collection operations;
- `'schedule'`: Triggered by scheduled tasks;
- `'action'`: Triggered by after-action events;
Extended trigger types need to ensure their identifiers are unique. The implementation for subscribing/unsubscribing the trigger is registered on the server-side, and the implementation for the configuration interface is registered on the client-side.
## Server-side
Any trigger needs to inherit from the `Trigger` base class and implement the `on`/`off` methods, which are used for subscribing to and unsubscribing from specific environment events, respectively. In the `on` method, you need to call `this.workflow.trigger()` within the specific event callback function to ultimately trigger the event. In the `off` method, you need to perform the relevant cleanup work for unsubscribing.
`this.workflow` is the workflow plugin instance passed into the `Trigger` base class's constructor.
```ts
import { Trigger } from '@nocobase/plugin-workflow';
class MyTrigger extends Trigger {
timer: NodeJS.Timeout;
on(workflow) {
// register event
this.timer = setInterval(() => {
// trigger workflow
this.workflow.trigger(workflow, { date: new Date() });
}, workflow.config.interval ?? 60000);
}
off(workflow) {
// unregister event
clearInterval(this.timer);
}
}
```
Then, in the plugin that extends the workflow, register the trigger instance with the workflow engine:
```ts
import WorkflowPlugin from '@nocobase/plugin-workflow';
export default class MyPlugin extends Plugin {
load() {
// get workflow plugin instance
const workflowPlugin = this.app.pm.get(WorkflowPlugin) as WorkflowPlugin;
// register trigger
workflowPlugin.registerTrigger('interval', MyTrigger);
}
}
```
After the server starts and loads, the `'interval'` type trigger can be added and executed.
## Client-side
The client-side part mainly provides a configuration interface based on the configuration items required by the trigger type. Each trigger type also needs to register its corresponding type configuration with the Workflow plugin.
For example, for the scheduled execution trigger mentioned above, define the required interval time configuration item (`interval`) in the configuration interface form:
```ts
import { Trigger } from '@nocobase/workflow/client';
class MyTrigger extends Trigger {
title = 'Interval timer trigger';
// fields of trigger config
fieldset = {
interval: {
type: 'number',
title: 'Interval',
name: 'config.interval',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
default: 60000,
},
};
}
```
Then, register this trigger type with the workflow plugin instance within the extended plugin:
```ts
import { Plugin } from '@nocobase/client';
import WorkflowPlugin from '@nocobase/plugin-workflow/client';
import MyTrigger from './MyTrigger';
export default class extends Plugin {
// You can get and modify the app instance here
async load() {
const workflow = this.app.pm.get(WorkflowPlugin) as WorkflowPlugin;
workflow.registerTrigger('interval', MyTrigger);
}
}
```
After that, the new trigger type will be visible in the workflow configuration interface.
:::info{title=Note}
The identifier of the trigger type registered on the client-side must be consistent with the one on the server-side, otherwise it will cause errors.
:::
For other details on defining trigger types, please refer to the [Workflow API Reference](./api#pluginregisterTrigger) section.
---
url: /plugins/@nocobase/plugin-acl/index.md
---
# Access Control
---
url: /plugins/@nocobase/plugin-action-bulk-edit/index.md
---
# Action: Bulk edit
---
url: /plugins/@nocobase/plugin-action-bulk-update/index.md
---
# Action: Bulk update
---
url: /plugins/@nocobase/plugin-action-custom-request/index.md
---
# Action: Custom request
---
url: /plugins/@nocobase/plugin-action-duplicate/index.md
---
# Action: Duplicate record
---
url: /plugins/@nocobase/plugin-action-export-pro/index.md
---
# Action: Export records Pro
---
url: /plugins/@nocobase/plugin-action-export/index.md
---
# Action: Export records
---
url: /plugins/@nocobase/plugin-action-import-pro/index.md
---
# Action: Import records Pro
---
url: /plugins/@nocobase/plugin-action-import/index.md
---
# Action: Import records
---
url: /plugins/@nocobase/plugin-action-print/index.md
---
# Action: Print
---
url: /plugins/@nocobase/plugin-action-template-print/index.md
---
# Print Template
---
url: /plugins/@nocobase/plugin-ai-gigachat/index.md
---
# AI LLM: GigaChat
---
url: /plugins/@nocobase/plugin-ai-knowledge-base/index.md
---
# AI: Knowledge base
---
url: /plugins/@nocobase/plugin-ai/index.md
---
# AI Employee
---
url: /plugins/@nocobase/plugin-api-doc/index.md
---
# API Documentation
---
url: /plugins/@nocobase/plugin-api-keys/index.md
---
# Authentication: API keys
---
url: /plugins/@nocobase/plugin-app-supervisor/index.md
---
# App Supervisor
---
url: /plugins/@nocobase/plugin-async-task-manager/index.md
---
# Asynchronous Task Manager
---
url: /plugins/@nocobase/plugin-audit-logger/index.md
---
# Audit logs
---
url: /plugins/@nocobase/plugin-audit-logs/index.md
---
# Audit logs (deprecated)
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-auth-cas/index.md
---
# Auth: CAS
---
url: /plugins/@nocobase/plugin-auth-dingtalk/index.md
---
# Auth: DingTalk
---
url: /plugins/@nocobase/plugin-auth-ldap/index.md
---
# Auth: LDAP
---
url: /plugins/@nocobase/plugin-auth-oidc/index.md
---
# Auth: OIDC
---
url: /plugins/@nocobase/plugin-auth-saml/index.md
---
# Auth: SAML 2.0
---
url: /plugins/@nocobase/plugin-auth-sms/index.md
---
# Authentication: SMS
---
url: /plugins/@nocobase/plugin-auth-wecom/index.md
---
# WeCom
---
url: /plugins/@nocobase/plugin-auth/index.md
---
# Authentication
---
url: /plugins/@nocobase/plugin-backup-restore/index.md
---
# App backup & restore (deprecated)
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-backups/index.md
---
# Backup manager
---
url: /plugins/@nocobase/plugin-block-grid-card/index.md
---
# Block: Grid Card
---
url: /plugins/@nocobase/plugin-block-iframe/index.md
---
# Block: iframe
---
url: /plugins/@nocobase/plugin-block-list/index.md
---
# Block: List
---
url: /plugins/@nocobase/plugin-block-markdown/index.md
---
# Markdown
---
url: /plugins/@nocobase/plugin-block-multi-step-form/index.md
---
# Block: Multi-step form
---
url: /plugins/@nocobase/plugin-block-template/index.md
---
# Block: template (deprecated)
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-block-tree/index.md
---
# Block: Tree
---
url: /plugins/@nocobase/plugin-block-workbench/index.md
---
# Block: Action panel
---
url: /plugins/@nocobase/plugin-calendar/index.md
---
# Calendar
---
url: /plugins/@nocobase/plugin-charts/index.md
---
# Charts (deprecated)
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-client/index.md
---
# Web Client
---
url: /plugins/@nocobase/plugin-collection-fdw/index.md
---
# Collection: Connect to foreign data (FDW)
---
url: /plugins/@nocobase/plugin-collection-sql/index.md
---
# Collection: SQL
---
url: /plugins/@nocobase/plugin-collection-tree/index.md
---
# Collection: Tree
---
url: /plugins/@nocobase/plugin-comments/index.md
---
# Comments
---
url: /plugins/@nocobase/plugin-custom-brand/index.md
---
# Custom brand
---
url: /plugins/@nocobase/plugin-custom-variables/index.md
---
# Custom variables
---
url: /plugins/@nocobase/plugin-data-source-external-clickhouse/index.md
---
# Data Source: External ClickHouse
---
url: /plugins/@nocobase/plugin-data-source-external-doris/index.md
---
# Data Source: External Doris
---
url: /plugins/@nocobase/plugin-data-source-external-mariadb/index.md
---
# Data source: External MariaDB
---
url: /plugins/@nocobase/plugin-data-source-external-mssql/index.md
---
# Data source: External SQL Server
---
url: /plugins/@nocobase/plugin-data-source-external-mysql/index.md
---
# Data source: External MySQL
---
url: /plugins/@nocobase/plugin-data-source-external-oracle/index.md
---
# Data Source: External Oracle
---
url: /plugins/@nocobase/plugin-data-source-external-postgres/index.md
---
# Data source: External PostgreSQL
---
url: /plugins/@nocobase/plugin-data-source-kingbase/index.md
---
# Data Source: KingbaseES
---
url: /plugins/@nocobase/plugin-data-source-main/index.md
---
# Data source: Main database
---
url: /plugins/@nocobase/plugin-data-source-manager/index.md
---
# Data Source Manager
---
url: /plugins/@nocobase/plugin-data-source-rest-api/index.md
---
# Data source: REST API
---
url: /plugins/@nocobase/plugin-data-visualization-echarts/index.md
---
# Data Visualization: ECharts
---
url: /plugins/@nocobase/plugin-data-visualization/index.md
---
# Data Visualization
---
url: /plugins/@nocobase/plugin-departments/index.md
---
# Departments
---
url: /plugins/@nocobase/plugin-email-manager/index.md
---
# Email Manager
---
url: /plugins/@nocobase/plugin-embed/index.md
---
# Embed NocoBase
---
url: /plugins/@nocobase/plugin-environment-variables/index.md
---
# Variables and secrets
---
url: /plugins/@nocobase/plugin-error-handler/index.md
---
# Error handler
---
url: /plugins/@nocobase/plugin-field-attachment-url/index.md
---
# Collection Field: Attachment (URL)
---
url: /plugins/@nocobase/plugin-field-china-region/index.md
---
# Collection field: China region
---
url: /plugins/@nocobase/plugin-field-code/index.md
---
# Collection field: Code
---
url: /plugins/@nocobase/plugin-field-component-mask/index.md
---
# Field Component: Mask
---
url: /plugins/@nocobase/plugin-field-encryption/index.md
---
# Collection field: Encryption
---
url: /plugins/@nocobase/plugin-field-formula/index.md
---
# Collection field: Formula
---
url: /plugins/@nocobase/plugin-field-m2m-array/index.md
---
# Collection field: Many-to-Many (M2M) (Array)
---
url: /plugins/@nocobase/plugin-field-markdown-vditor/index.md
---
# Collection field: Markdown(Vditor)
---
url: /plugins/@nocobase/plugin-field-sequence/index.md
---
# Collection field: Sequence
---
url: /plugins/@nocobase/plugin-field-sort/index.md
---
# Collection field: Sort
---
url: /plugins/@nocobase/plugin-file-manager/index.md
---
# File Manager
---
url: /plugins/@nocobase/plugin-file-previewer-office/index.md
---
# Office File Preview
---
url: /plugins/@nocobase/plugin-file-storage-s3-pro/index.md
---
# File storage: S3 (Pro)
---
url: /plugins/@nocobase/plugin-flow-engine/index.md
---
# Front-end flow engine
---
url: /plugins/@nocobase/plugin-form-drafts/index.md
---
# Form Drafts
---
url: /plugins/@nocobase/plugin-gantt/index.md
---
# Block: Gantt
---
url: /plugins/@nocobase/plugin-graph-collection-manager/index.md
---
# Graph collection manager
---
url: /plugins/@nocobase/plugin-ip-restriction/index.md
---
# IP restriction
---
url: /plugins/@nocobase/plugin-kanban/index.md
---
# Block: Kanban
---
url: /plugins/@nocobase/plugin-license/index.md
---
# License settings
---
url: /plugins/@nocobase/plugin-locale-tester/index.md
---
# Locale tester
---
url: /plugins/@nocobase/plugin-localization/index.md
---
# Localization
---
url: /plugins/@nocobase/plugin-lock-adapter-redis/index.md
---
# Redis lock adapter
---
url: /plugins/@nocobase/plugin-logger/index.md
---
# Logger
---
url: /plugins/@nocobase/plugin-map/index.md
---
# Block: Map
---
url: /plugins/@nocobase/plugin-migration-manager/index.md
---
# Migration Manager
---
url: /plugins/@nocobase/plugin-mobile-client/index.md
---
# Mobile Client (Deprecated)
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-mobile/index.md
---
# Mobile (deprecated)
---
url: /plugins/@nocobase/plugin-multi-app-manager/index.md
---
# Multi-app manager (deprecated)
---
url: /plugins/@nocobase/plugin-multi-app-share-collection/index.md
---
# Multi-app share collection
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-multi-keyword-filter/index.md
---
# Multi-keyword filter
---
url: /plugins/@nocobase/plugin-multi-space/index.md
---
# Multi-workspace
---
url: /plugins/@nocobase/plugin-notification-email/index.md
---
# Notification: Email
---
url: /plugins/@nocobase/plugin-notification-in-app-message/index.md
---
# Notification: In-app message
---
url: /plugins/@nocobase/plugin-notification-manager/index.md
---
# Notification Manager
---
url: /plugins/@nocobase/plugin-password-policy/index.md
---
# Password policy
---
url: /plugins/@nocobase/plugin-public-forms/index.md
---
# Public Forms
---
url: /plugins/@nocobase/plugin-pubsub-adapter-redis/index.md
---
# Redis pub sub adapter
---
url: /plugins/@nocobase/plugin-queue-adapter-rabbitmq/index.md
---
# RabbitMQ queue adapter
---
url: /plugins/@nocobase/plugin-queue-adapter-redis/index.md
---
# Redis queue adapter
---
url: /plugins/@nocobase/plugin-record-history/index.md
---
# Record history
---
url: /plugins/@nocobase/plugin-request-encryption/index.md
---
# HTTP request encryption
---
url: /plugins/@nocobase/plugin-snapshot-field/index.md
---
# Collection field: Association snapshot
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-system-settings/index.md
---
# System settings
---
url: /plugins/@nocobase/plugin-telemetry-prometheus/index.md
---
# Telemetry: Prometheus
---
url: /plugins/@nocobase/plugin-telemetry/index.md
---
# Telemetry
---
url: /plugins/@nocobase/plugin-text-copy/index.md
---
# Text Copy
---
url: /plugins/@nocobase/plugin-theme-editor/index.md
---
# Theme Editor
---
url: /plugins/@nocobase/plugin-two-factor-authentication/index.md
---
# Two-factor authentication (2FA)
---
url: /plugins/@nocobase/plugin-ui-schema-storage/index.md
---
# UI schema storage service
---
url: /plugins/@nocobase/plugin-ui-templates/index.md
---
# UI Templates
---
url: /plugins/@nocobase/plugin-user-data-sync/index.md
---
# User Data Synchronization
---
url: /plugins/@nocobase/plugin-users/index.md
---
# Users
---
url: /plugins/@nocobase/plugin-verification-totp-authenticator/index.md
---
# Verification: TOTP authenticator
---
url: /plugins/@nocobase/plugin-verification/index.md
---
# Verification
---
url: /plugins/@nocobase/plugin-workerid-allocator-redis/index.md
---
# Redis worker ID allocator
---
url: /plugins/@nocobase/plugin-workflow-action-trigger/index.md
---
# Workflow: Post-action event
---
url: /plugins/@nocobase/plugin-workflow-aggregate/index.md
---
# Workflow: Aggregate query node
---
url: /plugins/@nocobase/plugin-workflow-approval/index.md
---
# Workflow: Approval
---
url: /plugins/@nocobase/plugin-workflow-cc/index.md
---
# Workflow: CC
---
url: /plugins/@nocobase/plugin-workflow-custom-action-trigger/index.md
---
# Workflow: Custom action event
---
url: /plugins/@nocobase/plugin-workflow-date-calculation/index.md
---
# Workflow: Date Calculation Node
---
url: /plugins/@nocobase/plugin-workflow-delay/index.md
---
# Workflow: Delay node
---
url: /plugins/@nocobase/plugin-workflow-dynamic-calculation/index.md
---
# Workflow: Dynamic expression calculation node
> Note: This plugin is deprecated.
---
url: /plugins/@nocobase/plugin-workflow-javascript/index.md
---
# Workflow: JavaScript node
---
url: /plugins/@nocobase/plugin-workflow-json-query/index.md
---
# Workflow: JSON Query
---
url: /plugins/@nocobase/plugin-workflow-json-variable-mapping/index.md
---
# Workflow: JSON variable mapping
---
url: /plugins/@nocobase/plugin-workflow-loop/index.md
---
# Workflow: Loop node
---
url: /plugins/@nocobase/plugin-workflow-mailer/index.md
---
# Workflow: Send email node
---
url: /plugins/@nocobase/plugin-workflow-manual/index.md
---
# Workflow: Manual Node
---
url: /plugins/@nocobase/plugin-workflow-notification/index.md
---
# Workflow: Notification Node
---
url: /plugins/@nocobase/plugin-workflow-parallel/index.md
---
# Workflow: Parallel branch node
---
url: /plugins/@nocobase/plugin-workflow-request-interceptor/index.md
---
# Workflow: Pre-action event
---
url: /plugins/@nocobase/plugin-workflow-request/index.md
---
# Workflow: HTTP Request Node
---
url: /plugins/@nocobase/plugin-workflow-response-message/index.md
---
# Workflow: Response message
---
url: /plugins/@nocobase/plugin-workflow-sql/index.md
---
# Workflow: SQL Node
---
url: /plugins/@nocobase/plugin-workflow-subflow/index.md
---
# Workflow: Subflow
---
url: /plugins/@nocobase/plugin-workflow-test/index.md
---
# Workflow: Test Kit
---
url: /plugins/@nocobase/plugin-workflow-variable/index.md
---
# Workflow: Custom variable node
---
url: /plugins/@nocobase/plugin-workflow-webhook/index.md
---
# Workflow: Webhook Trigger
---
url: /plugins/@nocobase/plugin-workflow/index.md
---
# Workflow
---
url: /plugins/@nocobase/preset-cluster/index.md
---
# NocoBase Cluster
---
url: /plugins/index.md
---
---
url: /api/index.md
---
---
url: /api/auth/auth-manager.md
---
# AuthManager
## Overview
`AuthManager` is the user authentication management module in NocoBase, used to register different user authentication types.
### Basic Usage
```ts
const authManager = new AuthManager({
// Used to get the current authenticator identifier from the request header
authKey: 'X-Authenticator',
});
// Set the methods for AuthManager to store and retrieve authenticators
authManager.setStorer({
get: async (name: string) => {
return db.getRepository('authenticators').find({ filter: { name } });
},
});
// Register an authentication type
authManager.registerTypes('basic', {
auth: BasicAuth,
title: 'Password',
});
// Use the authentication middleware
app.resourceManager.use(authManager.middleware());
```
### Concepts
- **`AuthType`**: Different user authentication methods, such as password, SMS, OIDC, SAML, etc.
- **`Authenticator`**: The entity for an authentication method, actually stored in a collection, corresponding to a configuration record of a certain `AuthType`. One authentication method can have multiple authenticators, corresponding to multiple configurations, providing different user authentication methods.
- **`Authenticator name`**: The unique identifier for an authenticator, used to determine the authentication method for the current request.
## Class Methods
### `constructor()`
Constructor, creates an `AuthManager` instance.
#### Signature
- `constructor(options: AuthManagerOptions)`
#### Types
```ts
export interface JwtOptions {
secret: string;
expiresIn?: string;
}
export type AuthManagerOptions = {
authKey: string;
default?: string;
jwt?: JwtOptions;
};
```
#### Details
##### AuthManagerOptions
| Property | Type | Description | Default |
| --------- | --------------------------- | ------------------------------------- | ----------------- |
| `authKey` | `string` | Optional, the key in the request header that holds the current authenticator identifier. | `X-Authenticator` |
| `default` | `string` | Optional, the default authenticator identifier. | `basic` |
| `jwt` | [`JwtOptions`](#jwtoptions) | Optional, can be configured if using JWT for authentication. | - |
##### JwtOptions
| Property | Type | Description | Default |
| ----------- | -------- | ------------------ | ----------------- |
| `secret` | `string` | Token secret | `X-Authenticator` |
| `expiresIn` | `string` | Optional, token expiration time. | `7d` |
### `setStorer()`
Sets the methods for storing and retrieving authenticator data.
#### Signature
- `setStorer(storer: Storer)`
#### Types
```ts
export interface Authenticator = {
authType: string;
options: Record;
[key: string]: any;
};
export interface Storer {
get: (name: string) => Promise;
}
```
#### Details
##### Authenticator
| Property | Type | Description |
| ---------- | --------------------- | -------------- |
| `authType` | `string` | Authentication type |
| `options` | `Record` | Authenticator-related configuration |
##### Storer
`Storer` is the interface for authenticator storage, containing one method.
- `get(name: string): Promise` - Gets an authenticator by its identifier. In NocoBase, the actual returned type is [AuthModel](/auth-verification/auth/dev/api#authmodel).
### `registerTypes()`
Registers an authentication type.
#### Signature
- `registerTypes(authType: string, authConfig: AuthConfig)`
#### Types
```ts
export type AuthExtend = new (config: Config) => T;
type AuthConfig = {
auth: AuthExtend; // The authentication class.
title?: string; // The display name of the authentication type.
};
```
#### Details
| Property | Type | Description |
| ------- | ------------------ | --------------------------------- |
| `auth` | `AuthExtend` | The authentication type implementation, see [Auth](./auth) |
| `title` | `string` | Optional. The title of this authentication type displayed on the frontend. |
### `listTypes()`
Gets the list of registered authentication types.
#### Signature
- `listTypes(): { name: string; title: string }[]`
#### Details
| Property | Type | Description |
| ------- | -------- | ------------ |
| `name` | `string` | Authentication type identifier |
| `title` | `string` | Authentication type title |
### `get()`
Gets an authenticator.
#### Signature
- `get(name: string, ctx: Context)`
#### Details
| Property | Type | Description |
| ------ | --------- | ---------- |
| `name` | `string` | Authenticator identifier |
| `ctx` | `Context` | Request context |
### `middleware()`
Authentication middleware. Gets the current authenticator and performs user authentication.
---
url: /api/auth/auth.md
---
# Auth
## Overview
`Auth` is an abstract class for user authentication types. It defines the interfaces required to complete user authentication. To extend a new user authentication type, you need to inherit the `Auth` class and implement its methods. For a basic implementation, refer to: [BaseAuth](./base-auth.md).
```ts
interface IAuth {
user: Model;
// Check the authenticaiton status and return the current user.
check(): Promise;
signIn(): Promise;
signUp(): Promise;
signOut(): Promise;
}
export abstract class Auth implements IAuth {
abstract user: Model;
abstract check(): Promise;
// ...
}
class CustomAuth extends Auth {
// check: authentication
async check() {
// ...
}
}
```
## Instance Properties
### `user`
Authenticated user information.
#### Signature
- `abstract user: Model`
## Class Methods
### `constructor()`
Constructor, creates an `Auth` instance.
#### Signature
- `constructor(config: AuthConfig)`
#### Type
```ts
export type AuthConfig = {
authenticator: Authenticator;
options: {
[key: string]: any;
};
ctx: Context;
};
```
#### Details
##### AuthConfig
| Property | Type | Description |
| --------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `authenticator` | [`Authenticator`](./auth-manager#authenticator) | Authenticator data model. The actual type in a NocoBase application is [AuthModel](/auth-verification/auth/dev/api#authmodel). |
| `options` | `Record` | Authenticator-related configuration. |
| `ctx` | `Context` | Request context. |
### `check()`
User authentication. Returns user information. This is an abstract method that all authentication types must implement.
#### Signature
- `abstract check(): Promise`
### `signIn()`
User sign in.
#### Signature
- `signIn(): Promise`
### `signUp()`
User sign up.
#### Signature
- `signUp(): Promise`
### `signOut()`
User sign out.
#### Signature
- `signOut(): Promise`
---
url: /api/auth/base-auth.md
---
# BaseAuth
## Overview
`BaseAuth` inherits from the [Auth](./auth) abstract class and is the basic implementation for user authentication types, using JWT as the authentication method. In most cases, you can extend user authentication types by inheriting from `BaseAuth`, and there is no need to inherit directly from the `Auth` abstract class.
```ts
class BasicAuth extends BaseAuth {
constructor(config: AuthConfig) {
// Set the user collection
const userCollection = config.ctx.db.getCollection('users');
super({ ...config, userCollection });
}
// User authentication logic, called by `auth.signIn`
// Return user data
async validate() {
const ctx = this.ctx;
const { values } = ctx.action.params;
// ...
return user;
}
}
```
## Class Methods
### `constructor()`
Constructor, creates a `BaseAuth` instance.
#### Signature
- `constructor(config: AuthConfig & { userCollection: Collection })`
#### Details
| Parameter | Type | Description |
| :--- | :--- | :--- |
| `config` | `AuthConfig` | See [Auth - AuthConfig](./auth#authconfig) |
| `userCollection` | `Collection` | User collection, e.g., `db.getCollection('users')`. See [DataBase - Collection](../database/collection) |
### `user()`
Accessor, sets and gets user information. By default, it uses the `ctx.state.currentUser` object for access.
#### Signature
- `set user()`
- `get user()`
### `check()`
Authenticates via the request token and returns user information.
### `signIn()`
User sign-in, generates a token.
### `signUp()`
User sign-up.
### `signOut()`
User sign-out, expires the token.
### `validate()` *
Core authentication logic, called by the `signIn` interface, to determine if the user can sign in successfully.
---
url: /api/cli/cli.md
---
# NocoBase CLI
## yarn install
## yarn build
## yarn dev
## yarn start
## yarn test
## nocobase clean
## nocobase install
## nocobase upgrade
## nocobase pm
## nocobase create-migration
---
url: /api/cli/env.md
---
# Global Environment Variables
## TZ
Used to set the application's time zone, defaults to the operating system's time zone.
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
:::warning
Time-related operations will be processed according to this time zone. Modifying TZ may affect the date values in the database. For details, see '[Date & Time Overview](#)'
:::
## APP_ENV
Application environment, default value is `development`. Options include:
- `production` - Production environment
- `development` - Development environment
```bash
APP_ENV=production
```
## APP_KEY
The application's secret key, used for generating user tokens, etc. Change it to your own application key and ensure it is not disclosed.
:::warning
If APP_KEY is changed, old tokens will become invalid.
:::
```bash
APP_KEY=app-key-test
```
## APP_PORT
Application port, default value is `13000`.
```bash
APP_PORT=13000
```
## API_BASE_PATH
NocoBase API address prefix, default value is `/api/`.
```bash
API_BASE_PATH=/api/
```
## API_BASE_URL
## CLUSTER_MODE
> `v1.6.0+`
Multi-core (cluster) startup mode. If this variable is configured, it will be passed through to the `pm2 start` command as the `-i ` parameter. The options are consistent with the pm2 `-i` parameter (see [PM2: Cluster Mode](https://pm2.keymetrics.io/docs/usage/cluster-mode/)), including:
- `max`: use the maximum number of CPU cores
- `-1`: use the maximum number of CPU cores minus 1
- ``: specify the number of cores
The default value is empty, which means it is not enabled.
:::warning{title="Note"}
This mode needs to be used with cluster mode-related plugins, otherwise the application's functionality may be abnormal.
:::
For more information, see: [Cluster Mode](#).
## PLUGIN_PACKAGE_PREFIX
Plugin package name prefix, defaults to: `@nocobase/plugin-,@nocobase/preset-`.
For example, to add the `hello` plugin to the `my-nocobase-app` project, the full package name of the plugin would be `@my-nocobase-app/plugin-hello`.
PLUGIN_PACKAGE_PREFIX can be configured as:
```bash
PLUGIN_PACKAGE_PREFIX=@nocobase/plugin-,@nocobase-preset-,@my-nocobase-app/plugin-
```
Then the mapping between plugin names and package names is as follows:
- The package name for the `users` plugin is `@nocobase/plugin-users`
- The package name for the `nocobase` plugin is `@nocobase/preset-nocobase`
- The package name for the `hello` plugin is `@my-nocobase-app/plugin-hello`
## DB_DIALECT
Database type, options include:
- `mariadb`
- `mysql`
- `postgres`
```bash
DB_DIALECT=mysql
```
## DB_HOST
Database host (required when using MySQL or PostgreSQL database).
Default value is `localhost`.
```bash
DB_HOST=localhost
```
## DB_PORT
Database port (required when using MySQL or PostgreSQL database).
- MySQL, MariaDB default port 3306
- PostgreSQL default port 5432
```bash
DB_PORT=3306
```
## DB_DATABASE
Database name (required when using MySQL or PostgreSQL database).
```bash
DB_DATABASE=nocobase
```
## DB_USER
Database user (required when using MySQL or PostgreSQL database).
```bash
DB_USER=nocobase
```
## DB_PASSWORD
Database password (required when using MySQL or PostgreSQL database).
```bash
DB_PASSWORD=nocobase
```
## DB_TABLE_PREFIX
Table prefix.
```bash
DB_TABLE_PREFIX=nocobase_
```
## DB_UNDERSCORED
Whether to convert database table names and field names to snake case style, defaults to `false`. If you are using a MySQL (MariaDB) database and `lower_case_table_names=1`, then DB_UNDERSCORED must be `true`.
:::warning
When `DB_UNDERSCORED=true`, the actual table and field names in the database will not be consistent with what is seen in the interface. For example, `orderDetails` in the database will be `order_details`.
:::
## DB_LOGGING
Database logging switch, default value is `off`. Options include:
- `on` - Enabled
- `off` - Disabled
```bash
DB_LOGGING=on
```
## LOGGER_TRANSPORT
Log output transport, multiple values are separated by `,`. The default value in development environment is `console`, and in production environment is `console,dailyRotateFile`. Options:
- `console` - `console.log`
- `file` - `File`
- `dailyRotateFile` - `Daily rotating file`
```bash
LOGGER_TRANSPORT=console,dailyRotateFile
```
## LOGGER_BASE_PATH
File-based log storage path, defaults to `storage/logs`.
```bash
LOGGER_BASE_PATH=storage/logs
```
## LOGGER_LEVEL
Output log level. The default value in development environment is `debug`, and in production environment is `info`. Options:
- `error`
- `warn`
- `info`
- `debug`
- `trace`
```bash
LOGGER_LEVEL=info
```
The database log output level is `debug`, and whether it is output is controlled by `DB_LOGGING`, not affected by `LOGGER_LEVEL`.
## LOGGER_MAX_FILES
Maximum number of log files to keep.
- When `LOGGER_TRANSPORT` is `file`, the default value is `10`.
- When `LOGGER_TRANSPORT` is `dailyRotateFile`, use `[n]d` to represent days. The default value is `14d`.
```bash
LOGGER_MAX_FILES=14d
```
## LOGGER_MAX_SIZE
Rotate logs by size.
- When `LOGGER_TRANSPORT` is `file`, the unit is `byte`, and the default value is `20971520 (20 * 1024 * 1024)`.
- When `LOGGER_TRANSPORT` is `dailyRotateFile`, you can use `[n]k`, `[n]m`, `[n]g`. Not configured by default.
```bash
LOGGER_MAX_SIZE=20971520
```
## LOGGER_FORMAT
Log printing format. The default in development environment is `console`, and in production environment is `json`. Options:
- `console`
- `json`
- `logfmt`
- `delimiter`
```bash
LOGGER_FORMAT=json
```
See: [Log Format](#)
## CACHE_DEFAULT_STORE
The unique identifier for the cache store to use, specifying the server-side default cache store. Default value is `memory`. Built-in options:
- `memory`
- `redis`
```bash
CACHE_DEFAULT_STORE=memory
```
## CACHE_MEMORY_MAX
Maximum number of items in memory cache, default value is `2000`.
```bash
CACHE_MEMORY_MAX=2000
```
## CACHE_REDIS_URL
Redis connection, optional. Example: `redis://localhost:6379`
```bash
CACHE_REDIS_URL=redis://localhost:6379
```
## TELEMETRY_ENABLED
Enable telemetry data collection, defaults to `off`.
```bash
TELEMETRY_ENABLED=on
```
## TELEMETRY_METRIC_READER
Enabled monitoring metric readers, defaults to `console`. Other values should refer to the registered names of the corresponding reader plugins, such as `prometheus`. Multiple values are separated by `,`.
```bash
TELEMETRY_METRIC_READER=console,prometheus
```
## TELEMETRY_TRACE_PROCESSOR
Enabled trace data processors, defaults to `console`. Other values should refer to the registered names of the corresponding processor plugins. Multiple values are separated by `,`.
```bash
TELEMETRY_TRACE_PROCESSOR=console
```
---
url: /api/client/application.md
---
# Application
---
url: /api/client/plugin.md
---
# Plugin
## engine
## pm
---
url: /api/database/index.md
---
# Database
## Overview
Database is a database interaction tool provided by NocoBase, offering convenient database interaction capabilities for no-code and low-code applications. Currently supported databases are:
- SQLite 3.8.8+
- MySQL 8.0.17+
- PostgreSQL 10.0+
### Connect to Database
In the `Database` constructor, you can configure the database connection by passing in the `options` parameter.
```javascript
const { Database } = require('@nocobase/database');
// SQLite database configuration parameters
const database = new Database({
dialect: 'mysql',
host: 'localhost',
port: 3306,
database: 'nocobase',
username: 'root',
password: 'password'
})
// MySQL \ PostgreSQL database configuration parameters
const database = new Database({
dialect: /* 'postgres' or 'mysql' */,
database: 'database',
username: 'username',
password: 'password',
host: 'localhost',
port: 'port'
})
```
For detailed configuration parameters, please refer to [Constructor](#constructor).
### Data Model Definition
`Database` defines the database structure through `Collection`. A `Collection` object represents a table in the database.
```javascript
// Define Collection
const UserCollection = database.collection({
name: 'users',
fields: [
{
name: 'name',
type: 'string',
},
{
name: 'age',
type: 'integer',
},
],
});
```
After the database structure is defined, you can use the `sync()` method to synchronize the database structure.
```javascript
await database.sync();
```
For more detailed usage of `Collection`, please refer to [Collection](/api/database/collection).
### Data Read/Write
`Database` operates on data through `Repository`.
```javascript
const UserRepository = UserCollection.repository();
// Create
await UserRepository.create({
name: 'John',
age: 18,
});
// Query
const user = await UserRepository.findOne({
filter: {
name: 'John',
},
});
// Update
await UserRepository.update({
values: {
age: 20,
},
});
// Delete
await UserRepository.destroy(user.id);
```
For more detailed data CRUD usage, please refer to [Repository](/api/database/repository).
## Constructor
**Signature**
- `constructor(options: DatabaseOptions)`
Creates a database instance.
**Parameters**
| Parameter | Type | Default | Description |
| ---------------------- | -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------- |
| `options.host` | `string` | `'localhost'` | Database host |
| `options.port` | `number` | - | Database service port, with a default port corresponding to the database used |
| `options.username` | `string` | - | Database username |
| `options.password` | `string` | - | Database password |
| `options.database` | `string` | - | Database name |
| `options.dialect` | `string` | `'mysql'` | Database type |
| `options.storage?` | `string` | `':memory:'` | Storage mode for SQLite |
| `options.logging?` | `boolean` | `false` | Whether to enable logging |
| `options.define?` | `Object` | `{}` | Default table definition parameters |
| `options.tablePrefix?` | `string` | `''` | NocoBase extension, table name prefix |
| `options.migrator?` | `UmzugOptions` | `{}` | NocoBase extension, parameters related to the migration manager, refer to the [Umzug](https://github.com/sequelize/umzug/blob/main/src/types.ts#L15) implementation |
## Migration-related Methods
### `addMigration()`
Adds a single migration file.
**Signature**
- `addMigration(options: MigrationItem)`
**Parameters**
| Parameter | Type | Default | Description |
| -------------------- | ------------------ | ------ | ---------------------- |
| `options.name` | `string` | - | Migration file name |
| `options.context?` | `string` | - | Context of the migration file |
| `options.migration?` | `typeof Migration` | - | Custom class for the migration file |
| `options.up` | `Function` | - | `up` method of the migration file |
| `options.down` | `Function` | - | `down` method of the migration file |
**Example**
```ts
db.addMigration({
name: '20220916120411-test-1',
async up() {
const queryInterface = this.context.db.sequelize.getQueryInterface();
await queryInterface.query(/* your migration sqls */);
},
});
```
### `addMigrations()`
Adds migration files from a specified directory.
**Signature**
- `addMigrations(options: AddMigrationsOptions): void`
**Parameters**
| Parameter | Type | Default | Description |
| -------------------- | ---------- | -------------- | ---------------- |
| `options.directory` | `string` | `''` | Directory where migration files are located |
| `options.extensions` | `string[]` | `['js', 'ts']` | File extensions |
| `options.namespace?` | `string` | `''` | Namespace |
| `options.context?` | `Object` | `{ db }` | Context of the migration file |
**Example**
```ts
db.addMigrations({
directory: path.resolve(__dirname, './migrations'),
namespace: 'test',
});
```
## Utility Methods
### `inDialect()`
Checks if the current database type is one of the specified types.
**Signature**
- `inDialect(dialect: string[]): boolean`
**Parameters**
| Parameter | Type | Default | Description |
| --------- | ---------- | ------ | ------------------------------------------------ |
| `dialect` | `string[]` | - | Database type, possible values are `mysql`/`postgres`/`mariadb` |
### `getTablePrefix()`
Gets the table name prefix from the configuration.
**Signature**
- `getTablePrefix(): string`
## Collection Configuration
### `collection()`
Defines a collection. This call is similar to Sequelize's `define` method, creating the table structure only in memory. To persist it to the database, you need to call the `sync` method.
**Signature**
- `collection(options: CollectionOptions): Collection`
**Parameters**
All `options` configuration parameters are consistent with the constructor of the `Collection` class, refer to [Collection](/api/database/collection#constructor).
**Events**
- `'beforeDefineCollection'`: Triggered before defining a collection.
- `'afterDefineCollection'`: Triggered after defining a collection.
**Example**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'float',
name: 'price',
},
],
});
// sync collection as table to db
await db.sync();
```
### `getCollection()`
Gets a defined collection.
**Signature**
- `getCollection(name: string): Collection`
**Parameters**
| Parameter | Type | Default | Description |
| ------ | -------- | ------ | ---- |
| `name` | `string` | - | Collection name |
**Example**
```ts
const collection = db.getCollection('books');
```
### `hasCollection()`
Checks if a specified collection has been defined.
**Signature**
- `hasCollection(name: string): boolean`
**Parameters**
| Parameter | Type | Default | Description |
| ------ | -------- | ------ | ---- |
| `name` | `string` | - | Collection name |
**Example**
```ts
db.collection({ name: 'books' });
db.hasCollection('books'); // true
db.hasCollection('authors'); // false
```
### `removeCollection()`
Removes a defined collection. It is only removed from memory; to persist the change, you need to call the `sync` method.
**Signature**
- `removeCollection(name: string): void`
**Parameters**
| Parameter | Type | Default | Description |
| ------ | -------- | ------ | ---- |
| `name` | `string` | - | Collection name |
**Events**
- `'beforeRemoveCollection'`: Triggered before removing a collection.
- `'afterRemoveCollection'`: Triggered after removing a collection.
**Example**
```ts
db.collection({ name: 'books' });
db.removeCollection('books');
```
### `import()`
Imports all files in a directory as collection configurations into memory.
**Signature**
- `async import(options: { directory: string; extensions?: ImportFileExtension[] }): Promise