# Dynamic Logic

Screenshots on this page are taken from the Chinese UI. Menu, field and button names in the text use the English UI labels.

The system supports customization using the Groovy (opens new window) language. Groovy is a superset of Java: it supports Java syntax and adds more dynamic features, which makes it well suited to domain modeling and runtime enhancement.

TIP

If your team is not familiar with Groovy, you can also develop entirely in Java syntax. Groovy is highly compatible with Java.

# Target Audience

This document is intended for developers and implementers of the system.

# Structure of a Dynamic Logic Definition

Dynamic logic is maintained under the menu Development > Logics > Logics. The create form looks like this:

Create dynamic logic

Field Description
Name (name) Unique name of the logic; it cannot be changed after creation. Other objects (actions, scheduled tasks, forms, etc.) reference the logic by name or id
Logic engine (dynamicLogicEngine) The engine that executes the logic; see Dynamic Logic Engines below
Logic type (logicType) The scenario the logic is used in; see Dynamic Logic Types below. The dropdown shows the type's display name, for example Function logic
Description (description) Required; describes what the logic is for
Code (code) The logic code; its meaning depends on the logic engine
Revision (revision) Shown only in the edit form. The revision number increases by 1 each time the code is modified; see Revision History
History revisions, Comments Shown only in the edit form; they list the historical revisions and the comments respectively

In addition, dynamic logic has two fields that are not shown in the UI. They can only be set through CSV seed data or the data API (POST /data/DynamicLogic, PUT /data/DynamicLogic/{id}; the paths omit the /api prefix, see Address Prefix):

Field Default Description
enableLog false When true, every execution of the logic through runLogic (a form's Form Hook / Data Hook, custom function calls, etc.) writes a DynamicLogicExecuteRecord execution record containing the input parameters, the return value and the exception stack trace. There is no menu for these records in the UI; you can only query the dynamic_logic_execute_record table in the database. Object actions, object hooks and scheduled tasks do not go through this path; they have their own execution records
rateLimit empty Maximum number of executions per second (token-bucket rate limiting). Once exceeded, the call throws an exception with error code 1007 (RateLimitExceed). No rate limiting is applied when it is empty or not greater than 0

TIP

The names (name) of dynamic logic, object actions, object hooks, dynamic services and display themes cannot be changed after creation. The name of a scheduled task can be changed, but this is not recommended; see Scheduled Tasks for the reason.

# Dynamic Logic Engines

The system has 5 built-in execution engines (menu Development > Logics > Logic Engines):

Dynamic logic engine Description
GROOVY_CODE Executes the Groovy script in the Code field (plain Java syntax is also supported). This engine is used in the vast majority of scenarios
JVM_BYTECODE Executes precompiled JVM bytecode. The Code field holds JSON describing the bytecode and is not meant to be written by hand
OS_COMMAND Designed to execute operating system commands. Not usable in the current version; see the note below
RENDER_LINK Uses the same executor as GROOVY_CODE and behaves exactly the same
BEAN_EXECUTOR Executes a Java/Groovy class provided by a plugin; see below

OS_COMMAND is currently unusable

The command executed by the OS_COMMAND engine comes from an internal field that is never assigned in the current version, so execution fails immediately with an error. To call an external command, do it yourself in GROOVY_CODE logic (for example "ls -l".execute()).

# Using BEAN_EXECUTOR

BEAN_EXECUTOR lets you put the logic implementation in plugin code, which makes unit testing and IDE debugging easier:

  1. Implement the interface tech.muyan.api.logic.DynamicLogicExecutor in the plugin:
package com.example.logic;

import tech.muyan.api.logic.DynamicLogicExecutor;
import java.util.Map;

public class DiscountPriceExecutor implements DynamicLogicExecutor {
  @Override
  public Map<String, ?> execute(Map<String, Object> params) {
    // params contains all variables injected for this scenario, plus two extra parameters: logicName and logicType
    return Map.of("result", "OK");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
  1. Create a dynamic logic, select BEAN_EXECUTOR as the logic engine, and fill in only the fully qualified class name in the Code field, for example com.example.logic.DiscountPriceExecutor.

The platform loads the class from the plugin's ClassLoader and obtains the corresponding bean; if the class does not implement DynamicLogicExecutor, execution fails with an error. At execution time, in addition to the variables injected for the scenario, the platform passes two parameters: logicName (the logic name) and logicType (the logic type name).

# Dynamic Logic Development Guide

# Injected Variables

The code of a dynamic logic is a Groovy script. At runtime, the platform injects the context variables of the current scenario into the script. Different scenarios inject different variables; see the documentation of each scenario for details.

The following three variables are available in all GROOVY_CODE and JVM_BYTECODE logic:

Variable Type Description
application grails.core.GrailsApplication The current Grails application context
invoke Closure Calls a custom function; see Custom Functions for usage
logger org.slf4j.Logger A logger named DynamicLogic that writes to the platform backend log

Five scenarios โ€” object actions, object hooks, scheduled tasks, dynamic services and Webhooks โ€” also inject a log closure. Content written with log("...") is saved in the execution log of the corresponding execution record (the column is labeled "Execute log", or "Exec log" for dynamic services and Webhooks): for object actions, object hooks, scheduled tasks and dynamic services, view it under the menus Exec History > Action, Exec History > Object Hook, Exec History > Task and Exec History > Service respectively (dynamic services only save execution records when Enable Log is turned on); Webhook execution records are opened from the Exec records column of the Webhook list. Other scenarios such as a form's Form Hook and Data Hook and custom functions have no log variable; use logger instead.

# Return Value Convention

  • The return value of GROOVY_CODE / JVM_BYTECODE logic must be a Map. If any other type is returned (including null, a string or a number), the platform treats the return value as an empty Map [:].
  • Which keys the Map must contain is defined by each scenario; for example, object actions read execResult, scheduled tasks read execResult, and custom functions usually return result.
// Correct: return a Map
return [execResult: "ๅค„็†ๅฎŒๆˆ"]

// Wrong: returns a string, the caller receives an empty Map
return "ๅค„็†ๅฎŒๆˆ"
1
2
3
4
5

# Practical Examples

Here are some concrete business scenarios and example dynamic logic definitions:

# Automatic Customer Credit Limit Adjustment

Business scenario: when a customer is created, automatically set the credit limit based on the customer's initial credit rating.

Dynamic logic definition:

  • Logic type: OBJECT_DYNAMIC_HOOK, attached to a Before creating object hook on the customer object
  • Code: read object.creditRating, map the rating to a credit limit and assign it directly to object.creditLimit. Changes that a before-create hook makes to object are saved together with the object; see Object Hooks for details

# Form Field Linkage

Business scenario: in the order create form, after the user selects the "order type", automatically control whether the "delivery date" is visible and required.

Dynamic logic definition:

  • Logic type: FUNCTION_LOGIC (the platform's built-in Form Hook also uses this type), configured on DynamicForm.formHook
  • Trigger field: set formHookTriggerFields to orderType
  • Code: based on the values of changedFields and object, return a Map of field properties. For detailed usage, see Form Customization (Form Hook)

# Dynamic Logic Types

The system currently has the following 14 logic types (menu Development > Logics > Logic Types):

Type Dropdown label Scenario
OBJECT_DYNAMIC_HOOK Object Dynamic Hook Core logic of an object hook
DYNAMIC_ACTION_ENABLE_LOGIC Dynamic Action Enable Logic Enable logic of an object action
DYNAMIC_ACTION_LOGIC Dynamic Action Logic Core logic of an object action
FORM_GROUP_ENABLE_LOGIC Form Group Enable Logic Display logic of a form field group
WIZARD_CORE_LOGIC Wizard Core Logic Processing logic of a wizard
DASHBOARD_WIDGET_ENABLE_LOGIC Dashboard Widget Enable Logic Enable logic of a dashboard widget
DASHBOARD_WIDGET_CORE_LOGIC Dashboard Widget Core Logic Data logic of a dashboard widget
GANTT_RENDER_LOGIC Gantt row render logic Gantt chart row rendering
OBJECT_CLONE_CORE_LOGIC Clone clone core logic Object cloning
DYNAMIC_SERVICE_CORE_LOGIC Dynamic service core logic Core logic of a dynamic service
FUNCTION_LOGIC Function logic Custom functions; also used for a form's Form Hook / Data Hook
DYNAMIC_TASK_CORE_LOGIC Dynamic task core logic Core logic of a scheduled task
FIELD_DYNAMIC_HOOK Field Dynamic Hook Legacy type. Field-level hooks have been removed, and the platform no longer executes logic of this type
DYNAMIC_ACTION_POST_LOGIC Dynamic Action Post Logic Legacy type; the platform no longer executes it

The following pages describe in detail how dynamic logic is used in different business scenarios:

# Custom Functions

A custom function is a dynamic logic of type FUNCTION_LOGIC. It can be called directly from other dynamic logic and is used to capture reusable calculations or business rules.

# Defining a Function

Create a dynamic logic and select Function logic as the type (see the screenshot at the beginning of this page for the create form). The following example defines a function named "่ฎก็ฎ—ๆŠ˜ๆ‰ฃไปท" (calculate discounted price):

// ่ฎก็ฎ—ๆŠ˜ๆ‰ฃไปท: calculate the discounted price
// Parameters: price is the original price, discount is the discount rate (0.85 means 15% off)
BigDecimal finalPrice = (price as BigDecimal) * (discount as BigDecimal)
return [result: finalPrice.setScale(2, java.math.RoundingMode.HALF_UP)]
1
2
3
4

The parameters passed by the caller are injected into the function code as variables; price and discount here are the parameters passed in the call. A function must also return a Map; otherwise the caller receives an empty Map.

# Calling a Function

In any GROOVY_CODE logic, call the function directly with the invoke closure injected by the platform:

// The first argument is the name of the function (dynamic logic), the second is the Map of parameters to pass
Map res = invoke("่ฎก็ฎ—ๆŠ˜ๆ‰ฃไปท", [price: 199.00, discount: 0.85])
// res is the Map returned by the function, here [result: 169.15]
1
2
3

In plugin code, or anywhere the invoke variable is not available, you can also call it through the bean, with the same effect:

import tech.muyan.BeanHelper
import tech.muyan.api.DynamicFunctionService

Map res = BeanHelper.getBean(DynamicFunctionService).invoke("่ฎก็ฎ—ๆŠ˜ๆ‰ฃไปท", [price: 199.00, discount: 0.85]) as Map
1
2
3
4

The following object action demonstrates the call (for how to configure object actions, see Object Actions). First create a dynamic logic "ๆŠ˜ๆ‰ฃไปท่ฏ•็ฎ—" (discounted price trial calculation) of type Dynamic Action Logic as the action's core logic. It calls the "่ฎก็ฎ—ๆŠ˜ๆ‰ฃไปท" function and returns the result as the action's execution result:

// Call the custom function "่ฎก็ฎ—ๆŠ˜ๆ‰ฃไปท"
Map res = invoke("่ฎก็ฎ—ๆŠ˜ๆ‰ฃไปท", [price: 199.00, discount: 0.85])
log("ๅ‡ฝๆ•ฐ่ฟ”ๅ›ž๏ผš${res}")
return [execResult: "ๅŽŸไปท 199.00 ๅ…ƒ๏ผŒๅ…ซไบ”ๆŠ˜ๅŽไธบ ${res.result} ๅ…ƒ"]
1
2
3
4

Calling a function in the action's core logic

Then create a CLASS_LEVEL object action "ๆŠ˜ๆ‰ฃไปท่ฏ•็ฎ—", select the logic above as its core logic, and set its Ext info to {"resultType": "inContainer"} so that the execution result is shown in a modal (if it is not set, the result is shown as a message notification at the top of the page). The action must also have an access requirement (accessRequirement), otherwise it does not appear in the list. This field is not available in the UI; see Object Actions for how to set it. Finally, attach the action to the list form of "User", and a "ๆŠ˜ๆ‰ฃไปท่ฏ•็ฎ—" button appears above the list:

Action button on the list

Click the button to execute it; the modal shows the discounted price calculated by the function:

Execution result

WARNING

invoke looks up a dynamic logic of type FUNCTION_LOGIC by name. If none is found, it fails with ErrorCode: 15001, ErrorMsg: FunctionNotFound, ExtInfo: <function name>.

The platform loads all functions into memory at startup; creating or modifying a function afterwards updates them in sync. The current version has two known issues: after a function is deleted, or after its type is changed to another type, it can still be called by invoke until the backend restarts.

# Calling External HTTP APIs

In dynamic logic you can call the HTTP APIs of external systems directly with the JDK's built-in java.net.http.HttpClient or Groovy's URL API, with no extra dependencies. Built-in capabilities removed in 1.0, such as the LLM engine and email sending, can be replaced this way by integrating external services, for example by calling the HTTP API of an LLM service or an email service provider.

It is recommended to wrap the call in a custom function of type FUNCTION_LOGIC and have other logic call it through invoke. The function "่ฐƒ็”จๅค–้ƒจๆŽฅๅฃ" (call external API) below takes two parameters: url is the API address and payload is the request body (a Map). When payload is null, it sends a GET request; otherwise it sends a POST request in JSON format:

import groovy.json.JsonOutput
import groovy.json.JsonSlurper

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration

HttpClient client = HttpClient.newBuilder()
  .connectTimeout(Duration.ofSeconds(5))
  .build()

HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url as String))
  .timeout(Duration.ofSeconds(10))
  .header('Content-Type', 'application/json')
if (payload != null) {
  builder.POST(HttpRequest.BodyPublishers.ofString(JsonOutput.toJson(payload)))
} else {
  builder.GET()
}
HttpResponse<String> response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString())
if (response.statusCode() >= 300) {
  throw new RuntimeException("ๅค–้ƒจๆŽฅๅฃ่ฟ”ๅ›ž ${response.statusCode()}๏ผš${response.body()}")
}
// An empty response body (e.g. 204) must not be passed to JsonSlurper, otherwise it throws IllegalArgumentException
String body = response.body()
return [status: response.statusCode(), result: body ? new JsonSlurper().parseText(body) : null]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Call it from other dynamic logic. Both parameters must be passed; for a GET request, pass null as payload:

Map res = invoke("่ฐƒ็”จๅค–้ƒจๆŽฅๅฃ", [
  url    : "https://api.example.com/orders",
  payload: [orderNo: "SO-20260923-001", amount: 199.00]
])
def data = res.result   // JSON returned by the external API, already parsed into a Map / List; null when the response body is empty
1
2
3
4
5

If you only need a simple GET request, you can also use Groovy's URL API:

import groovy.json.JsonSlurper

String text = new URL("https://api.example.com/status").getText(
  connectTimeout: 5000,
  readTimeout: 10000,
  requestProperties: [Accept: 'application/json']
)
def data = new JsonSlurper().parseText(text)
1
2
3
4
5
6
7
8

Notes:

  • Requests are sent from the machine (or container) where the backend service runs, so the API address must be reachable from there. In a Docker deployment, localhost refers to the backend container itself.
  • Always set timeouts. Scenarios such as object actions and Form Hooks run synchronously, so a slow external API directly slows down UI operations.
  • In the example above, an exception is thrown when the external API returns a non-2xx status code, and the caller fails as a result; for example, the execution status of an object action becomes FAILED. Use try/catch at the call site if you need fault tolerance.
  • Do not put API keys in the code. You can store them in System Configuration and read them with ConfigHelper.dynamicConfigService.getByKey("config key"), or provide them through environment variables.

# Revision History

Dynamic logic keeps a revision history:

  • Each time the Code field is modified, the revision number increases by 1 and the previous content is saved as a historical revision, which can be viewed under "History revisions" in the edit form.
  • In the list of historical revisions, use "Latest revision changes" to see the differences between a historical revision and the current version, "Compare revision" to compare two selected historical revisions, and "Set as current" to restore a historical revision as the current code (rollback).

WARNING

In the current version, deleting a dynamic logic is a physical delete; deleted logic does not appear under the Trash > Logic Trash menu. For a logic that already has historical revisions, you must delete its historical revisions before you can delete it.

# Auto-Refreshing Code During Development

When the platform is started with the JVM system property -DdynamicLogic.autoRefreshSeedDataFolder=true, it reads the files in the csv subdirectory of the seed data directory whose names match ^(\d*-)?DynamicLogic(_.*)?\.csv$ (for example DynamicLogic.csv, 004-DynamicLogic.csv, DynamicLogic_task.csv). These files must contain the name(*) and code(F) columns. The platform watches the source files referenced in the code(F) column and, when a file is modified, automatically refreshes the code of the corresponding logic in the database, which is convenient for local development and debugging. Do not enable this in production.

Last Updated: 9/24/2026, 2:27:35 PM