iComposer & AI
iComposer
iComposer Hooks
Sidebar On this page

This guide is for business developers writing Hook scripts on the iComposer platform. It covers the concepts, authoring workflow, and verification methods for the three types of Hooks.

1. Overview: What are Hooks?

iComposer Hooks are a hot-deployable Groovy script extension mechanism that lets tenants inject long-lived, runtime-resident custom logic without restarting the service:

  • Listen to messages (iComposer Listener)
  • Participate in distributed transactions (TCC)
  • React to cluster-wide cache changes (Clear Cache Listener)

Unlike regular Groovy APIs / ServiceFunctions (request-driven and executed only when invoked), a Hook continuously listens for events after registration and is triggered automatically when an event arrives.

Key characteristics

  • Script-based: Write Groovy method bodies in the UI editor; saving compiles immediately.
  • Hot reload: After a new version is published, no restart is needed — the platform automatically unloads the old script and loads the new one via cache events.
  • Tenant-isolated: Each Hook belongs to a tenant/module and is isolated by tenant context at registration time.
  • Auto-recovery on startup: When the runtime service restarts, it automatically pulls all active Hooks from the database and re-registers them.

2. The three Hook types at a glance

TypeCategory IDHandler interfaceWhen triggeredTypical scenario
iComposer Listener1ListenerHandlerMQ message arrival (Kafka / RabbitMQ)Asynchronously consume order / notification queues
TCC2TccHandlerSeata global transaction prepare / commit / rollbackCross-service distributed transactions: reserve / confirm / roll back resources
Clear Cache Listener3ClearCacheHandlerCluster-wide cache change eventClear local caches when upstream configuration changes

3. Authoring and deploying Hooks in the UI

All three Hook types share the same authoring and deployment flow — learn it once.

3.1 Entry point and steps

  1. Open the iComposer designer and go to the Hooks management page.
  2. Create a new hook and choose a category:
    • 1 → iComposer Listener
    • 2 → TCC
    • 3 → Clear Cache Listener
  3. After choosing a category, the editor automatically applies the corresponding code template as a starting point.
  4. Write/modify the Groovy method body in the “user code area”.
  5. Save and set the status to ACTIVE; the platform immediately triggers compilation.

3.2 Built-in capabilities available in the script

BaseMonitorGroovySupport provides the following out-of-the-box methods for every Hook:

PurposeCall
Get a Spring BeangetBean("beanName") or getBean(SomeType.class)
Logginginfo(...) / warn(...) / error(...) / debug(...)

In addition, utility classes under com.insuremo.icomposer.utils (such as IComposerHooksUtils, IComposerEnv) can be imported and used directly in scripts. See each type’s section below for details.

4. Type 1: iComposer Listener tutorial

4.1 Responsibility

Listen continuously (Kafka / RabbitMQ) and hand each message off to a ServiceFunction for processing.

4.2 Interface and methods

MethodWhen calledWhat you do
void init()On application startup / after hot-reload registrationRead MQ config, create the Listener container, register it with the Registry
void destroy()On unregister / shutdown / hot-reload unloadRelease custom resources

You may also define extra methods (e.g. send(data) as a companion producer); the platform will not interfere.

4.3 When triggered

Consumption starts automatically after the Spring container starts; each time a message arrives your message callback is triggered.

4.4 Three-step authoring

Read config IComposerEnv.getParameter("mq.<business>.xxx")
Build container Build a ConnectionFactory / ConsumerFactory + Listener container;
in the message callback, use IComposerHooksUtils.executeMethod(functionName, methodName, body)
to hand the message off to a ServiceFunction
Register IComposerHooksUtils.registerResource(this, [container: container])

Key points:

  • Lifecycle ordering: Before calling your destroy(), the platform first calls stop() on any container registered via registerResource (waiting for in-flight messages to finish processing). Therefore destroy() only needs to release custom resources outside the container (e.g. a self-created CachingConnectionFactory).
  • registerResource(this, [container: container]) automatically extracts and manages the container (a Spring Lifecycle); the resource ID is your class name with the Gry_Monitor_ prefix removed.
  • For executeMethod(functionName, methodName, args...), when methodName is null it defaults to execute.
  • Always try/catch inside the message callback and log exceptions with error(...) to avoid silently swallowing them.
  • Multiple MQ connections: each MonitorTask script uses its own mq.<business>.* config-center parameters, independent of one another.

4.5 Full example: Kafka Listener

import org.springframework.kafka.listener.ConcurrentMessageListenerContainer
import org.springframework.kafka.listener.ContainerProperties
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
import com.insuremo.icomposer.utils.IComposerHooksUtils
import com.insuremo.icomposer.utils.IComposerEnv

private ConcurrentMessageListenerContainer container

void init() {
// 1. Read Kafka connection params from the config center (do not hardcode)
def bootstrapServers = IComposerEnv.getParameter("mq.claim.bootstrapServers")
def topic = IComposerEnv.getParameter("mq.claim.topic")
def groupId = IComposerEnv.getParameter("mq.claim.groupId")
def consumers = IComposerEnv.getParameter("mq.claim.consumers", "3") as int

// 2. Build the ConsumerFactory + Listener container
def props = new HashMap<String, Object>()
props.put("bootstrap.servers", bootstrapServers)
props.put("group.id", groupId)
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
props.put("auto.offset.reset", "latest")
def consumerFactory = new DefaultKafkaConsumerFactory<String, String>(props)

def containerProps = new ContainerProperties(topic)
containerProps.setGroupId(groupId)

container = new ConcurrentMessageListenerContainer(consumerFactory, containerProps)
container.getContainerProperties().setMessageListener({ record ->
try {
// Hand the message off to a ServiceFunction for processing
IComposerHooksUtils.executeMethod("ClaimHandlerFunction", "execute", record.value())
} catch (Exception e) {
error("Failed to process message: ${e.message}"); e.printStackTrace()
}
})
container.setConcurrency(consumers)

// 3. Register with the Registry (container is managed automatically)
IComposerHooksUtils.registerResource(this, [container: container])
info("Kafka ClaimListenerHandler started")
}

void destroy() {
// The framework already called lifecycle.stop() to wait for in-flight messages;
// release only custom resources here
info("Kafka ClaimListenerHandler destroyed")
}

Configure in the config center: mq.claim.bootstrapServers, mq.claim.topic, mq.claim.groupId, mq.claim.consumers.

4.6 Variant: RabbitMQ Listener

Replace the Kafka parts of the example above with RabbitMQ; the structure is identical:

import org.springframework.amqp.rabbit.connection.CachingConnectionFactory
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer
import com.insuremo.icomposer.utils.IComposerHooksUtils
import com.insuremo.icomposer.utils.IComposerEnv

private CachingConnectionFactory connectionFactory
private SimpleMessageListenerContainer container

void init() {
def host = IComposerEnv.getParameter("mq.order.host")
def port = IComposerEnv.getParameter("mq.order.port", "5672") as int
def username = IComposerEnv.getParameter("mq.order.username")
def password = IComposerEnv.getParameter("mq.order.password")
def virtualHost = IComposerEnv.getParameter("mq.order.virtualHost", "/")
def queueName = IComposerEnv.getParameter("mq.order.queue")
def consumers = IComposerEnv.getParameter("mq.order.consumers", "3") as int

connectionFactory = new CachingConnectionFactory(host, port)
connectionFactory.setUsername(username)
connectionFactory.setPassword(password)
connectionFactory.setVirtualHost(virtualHost)

container = new SimpleMessageListenerContainer()
container.setConnectionFactory(connectionFactory)
container.setQueueNames(queueName)
container.setConcurrentConsumers(consumers)
container.setMessageListener({ message ->
try {
def body = new String(message.getBody(), "UTF-8")
IComposerHooksUtils.executeMethod("OrderHandlerFunction", "execute", body)
} catch (Exception e) {
error("Failed to process message: ${e.message}"); e.printStackTrace()
}
})

IComposerHooksUtils.registerResource(this, [container: container])
info("RabbitMQ OrderListenerHandler started")
}

void destroy() {
if (connectionFactory != null) {
connectionFactory.destroy()
}
info("RabbitMQ OrderListenerHandler destroyed")
}

Configure in the config center: mq.order.host, mq.order.port, mq.order.username, mq.order.password, mq.order.virtualHost, mq.order.queue, mq.order.consumers.

4.7 How to verify

  1. After saving the script (ACTIVE), make sure the application has started or finished hot-reloading.
  2. Send a test message to the target queue / topic.
  3. Check the application logs: you should see the started log line, plus the ServiceFunction’s execution log when a message arrives.

5. Type 2: TCC distributed transaction tutorial

5.1 Responsibility

Act as a branch participant in a Seata TCC (Try-Confirm-Cancel) distributed transaction, reserving resources in the Try phase and letting the transaction coordinator decide the final commit or rollback.

5.2 Interface and methods

MethodPhaseReturnDescription
Boolean prepare(IComposerTccActionContext ctx)TryBooleanReserve resources (freeze an amount, lock stock, etc.)
Boolean commit(IComposerTccActionContext ctx)ConfirmBooleanConfirm resources (deduct, confirm payment, etc.)
Boolean rollback(IComposerTccActionContext ctx)CancelBooleanRelease the reserved resources

The return type is Boolean (the wrapper type), not the primitive boolean. Returning true means success; throwing an exception or returning false means failure.

5.3 When triggered

  • prepare: Triggered explicitly by business code calling IComposerHooksUtils.prepareTcc("actionName", params).
  • commit / rollback: Coordinated and called by the Seata TC (transaction coordinator) when the global transaction ends; not called directly by business code.

5.4 Key API: IComposerTccActionContext

MethodReturnDescription
getParams()StringThe business-parameter JSON passed in during prepare
getXid()StringGlobal transaction ID
getActionName()StringName of the current TCC action

5.5 Authoring tips

  • Resources reserved in prepare must be confirmed in commit and released in rollback — the three must agree.
  • commit / rollback must be idempotent: Seata may retry these two methods due to timeouts or other reasons.
  • getParams() returns a JSON string; parse it yourself as needed.

5.6 Full example: freeze / confirm / unfreeze an amount

import com.insuremo.icomposer.hooks.tcc.IComposerTccActionContext
import com.insuremo.icomposer.utils.IComposerJsonUtils

// Try: freeze the amount
Boolean prepare(IComposerTccActionContext ctx) {
def params = new IComposerJsonUtils().parseText(ctx.getParams())
def xid = ctx.getXid()
info("TCC prepare: xid=${xid}, orderId=${params.orderId}, amount=${params.amount}")

// Business logic: validate and freeze the amount
// orderService.freezeAmount(params.orderId as String, params.amount as BigDecimal)
return true
}

// Confirm: confirm the deduction
Boolean commit(IComposerTccActionContext ctx) {
def xid = ctx.getXid()
info("TCC commit: xid=${xid}, action=${ctx.getActionName()}")

// Business logic: confirm-deduct the amount frozen in prepare (must be idempotent)
// (prepare usually records the freeze info keyed by xid; confirm accordingly)
return true
}

// Cancel: unfreeze and release
Boolean rollback(IComposerTccActionContext ctx) {
def xid = ctx.getXid()
info("TCC rollback: xid=${xid}, action=${ctx.getActionName()}")

// Business logic: release the amount frozen in prepare (must be idempotent)
// (prepare usually records the freeze info keyed by xid; release accordingly)
return true
}

How business code triggers prepare (written in another api/ServiceFunction):

import com.insuremo.icomposer.utils.IComposerHooksUtils

def params = [orderId: "ORD-001", amount: 100]
boolean ok = IComposerHooksUtils.prepareTcc("FreezeAmountAction", params)
// ok == true means prepare succeeded; commit / rollback are then coordinated by Seata TC when the global transaction ends

5.7 How to verify

  1. Issue a business request that calls prepareTcc("FreezeAmountAction", ...).
  2. Success path: all branches’ prepare succeed → observe the prepare and commit logs.
  3. Failure path: deliberately make one branch’s prepare fail or throw → observe the prepare and rollback logs.

6. Type 3: ClearCache cache-event tutorial

6.1 Responsibility

Listen for the platform’s cluster-wide cache change events. When another node publishes a configuration / product / rule change (triggering cluster-wide cache sync), this node clears or refreshes its locally derived caches accordingly.

6.2 Interface and methods

Implement com.insuremo.unicorn.platform.groovy.monitor.cache.clear.ClearCacheHandler:

void onCacheChanged(IComposerCacheChangeEvent event,
String bucketName,
String cacheName,
Object cacheKey)
ParameterMeaning
eventCache change event; carries tenant, change type, etc.
bucketNameCache bucket name
cacheNameCache name
cacheKeyThe changed cache key; null means clear-all

6.3 When triggered

After the iComposer service receives a platform cache-sync event, it dispatches to all registered ClearCacheHandlers. Triggered on every cache change.

6.4 Key API: IComposerCacheChangeEvent

MethodDescription
getType()Change type (e.g. CLEAR for clear-all / UPDATE for a single entry)
getTenantCode()Tenant code

6.5 Authoring tips

  • onCacheChanged receives all cache change events; filter by bucketName / cacheName for the ones you care about.
  • cacheKey == null means clear-all; in that case you should refresh the entire local cache.
  • A single handler throwing an exception does not affect other handlers (the dispatcher calls them one by one and isolates failures).

6.6 Full example: clear locally derived caches by cache name

import com.insuremo.icomposer.hooks.cache.clear.IComposerCacheChangeEvent

void onCacheChanged(IComposerCacheChangeEvent event,
String bucketName,
String cacheName,
Object cacheKey) {
info("Cache changed: bucket=${bucketName}, cache=${cacheName}, key=${cacheKey}, type=${event.getType()}, tenant=${event.getTenantCode()}")

if (cacheKey == null) {
// Clear-all
// productLocalCache.invalidateAll()
return
}
// Branch by cache name
if (cacheName.contains("ProductCache")) {
// productLocalCache.invalidate(cacheKey)
} else if (cacheName.contains("RuleCache")) {
// ruleEngine.reload(cacheKey)
}
}

7. FAQ & troubleshooting

Q1: Saved but not taking effect? Check whether the status is ACTIVE; check whether compilation reported errors (the return info when saving); confirm the runtime has hot-reloaded (use the status API in 8.2 to see whether the resource is registered). If necessary, bump the version number and save again to trigger cache invalidation.

Q2: Should I write a full class, or just the method body? Write only a method-body fragment (imports + methods). The platform’s MonitorTaskBeanGenerator automatically wraps it in a class Gry_Monitor_<name> extends BaseMonitorGroovySupport implements <corresponding Handler> shell. Adding an outer class yourself will instead cause a conflict.

Q3: How do I get a Spring Bean / log in a script? Use getBean("xxx") or getBean(SomeType.class) to get a Bean; use info(...) / warn(...) / error(...) / debug(...) for logging — these are provided by BaseMonitorGroovySupport.

Q4: What if one Hook needs to connect to multiple MQs? Give each MonitorTask script its own mq.<business>.* config-center parameters; each creates its own connection inside the script, independently.

Q5: TCC commit / rollback not being called? Check: ① did prepare return true; ② is Seata TC running normally; ③ confirm that commit/rollback is coordinated by TC, not called directly by business code.

Q6: What happens if Listener consumption throws an exception? try/catch inside the message callback and log it with error(...). Before destroy(), the framework first calls lifecycle.stop() to wait for in-flight messages to finish — it will not abort abruptly.

Q7: Too many events in onCacheChanged? This is normal — it receives every cache change. Filter by bucketName / cacheName inside the method; cacheKey == null means clear-all.

Appendix: Cheat sheet for the three Hook types

DimensioniComposer ListenerTCCClear Cache Listener
Category ID123
Handler interfaceListenerHandlerTccHandlerClearCacheHandler
Key methodsinit() / destroy()prepare / commit / rollback(ctx) returning BooleanonCacheChanged(event, bucketName, cacheName, cacheKey)
Triggered bySpring MQ container (message arrival)Business calls IComposerHooksUtils.prepareTcc + Seata TC coordinationIComposerCacheChangeListener dispatch
Register / trigger APIIComposerHooksUtils.registerResource(...)IComposerHooksUtils.prepareTcc(...)Auto-dispatched by the platform; no manual registration


Feedback
Was this page helpful?
|
Provide feedback