Event SDK
The MetaOne event system is workspace-scoped and supports both in-process (async) and external (webhook-style) delivery.
Core Types
PlatformEvent
The event record passed to all handlers.
public record PlatformEvent(
String eventKey, // e.g. "crm.deal.closed"
String schemaVersion, // Payload schema version, e.g. "1.0"
String tenantId, // Workspace ID (same as workspaceId)
String requestId, // Request that triggered this event
String actor, // User or system that published
Instant occurredAt, // When the event occurred
Map<String, Object> payload // Arbitrary key-value event data
)EventPayload
A marker interface for typed payload DTOs. Implement it in your payload record and the platform will deserialize the raw Map<String, Object> payload into your type automatically.
public interface EventPayload { }Example typed payload:
public record DealClosedPayload(
String dealId,
String amount,
String currency
) implements EventPayload {}EventBusGateway
Injected into the plugin Spring context by the host. Use it to publish events from within a plugin.
public interface EventBusGateway {
void publish(PlatformEvent event);
}Annotations
@EventHandler
Marks a method as the handler for a specific event key.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventHandler {
String eventKey(); // Full event key, e.g. "crm.deal.closed"
boolean async() default true; // Run on dedicated thread pool?
}Method signature rules:
- First parameter:
PlatformEventOR anyEventPayloadsubtype - Second parameter:
CapabilityContext
@EventGroup
Optional class-level grouping annotation for a class containing @EventHandler methods.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventGroup {
String description() default "";
}PlatformEventHandler
Marker interface. Extend it in your event listener class to make it discoverable by the platform (via PF4J).
public interface PlatformEventHandler extends ExtensionPoint { }Writing an Event Handler
import org.pf4j.Extension;
import vn.metaone.sdk.event.*;
import vn.metaone.sdk.capability.CapabilityContext;
@Extension
@EventGroup(description = "Handles CRM deal events")
public class DealEventListener implements PlatformEventHandler {
private final NotificationService notificationService;
public DealEventListener(NotificationService notificationService) {
this.notificationService = notificationService;
}
// Typed payload — platform deserializes Map → DealClosedPayload
@EventHandler(eventKey = "crm.deal.closed", async = true)
public void onDealClosed(DealClosedPayload payload, CapabilityContext ctx) {
notificationService.notify(
ctx.workspaceId(),
"Deal " + payload.dealId() + " closed for " + payload.amount()
);
}
// Raw event — access the full PlatformEvent
@EventHandler(eventKey = "platform.notification", async = false)
public void onNotification(PlatformEvent event, CapabilityContext ctx) {
String channel = (String) event.payload().get("channel");
String message = (String) event.payload().get("message");
// route to the right conversation...
}
}Publishing Events from a Plugin
Inject EventBusGateway via Spring and call publish():
@Service
public class DealService {
private final EventBusGateway eventBus;
public DealService(EventBusGateway eventBus) {
this.eventBus = eventBus;
}
public void closeDeal(String workspaceId, String dealId, BigDecimal amount) {
// ... business logic ...
eventBus.publish(new PlatformEvent(
"crm.deal.closed",
"1.0",
workspaceId,
UUID.randomUUID().toString(),
"crm-bot",
Instant.now(),
Map.of(
"dealId", dealId,
"amount", amount.toPlainString(),
"currency", "USD"
)
));
}
}Cross-Extension Notifications
Any extension can post a message to a chat channel — without depending on the chat extension — by publishing a platform.notification event:
eventBus.publish(new PlatformEvent(
"platform.notification",
"1.0",
workspaceId,
requestId,
"crm-bot",
Instant.now(),
Map.of(
"channel", "deals", // Well-known: "general", "deals", or a conversationId
"message", "Deal #" + dealId + " is closed!",
"sender", "crm-bot"
)
));Well-known channels:
| Channel | Description |
|---|---|
general | General workspace notifications |
deals | CRM deal activity feed |
{conversationId} | A specific conversation |
Registering Event Subscriptions
Declare consumed events in the manifest. The platform creates ext_event_subscription rows during install:
new CapabilityManifest(
List.of(...), // provides
List.of("crm.deal.closed", // consumesEvents — platform will route these here
"platform.notification"),
List.of("crm.deal.closed") // publishesEvents — informational
)Async vs Sync Handlers
async | Behavior |
|---|---|
true (default) | Runs on a dedicated eventExecutor thread pool. Does not block the event publisher. Workspace context is set on the thread before dispatch. |
false | Runs inline on the publisher's thread. Blocks until handler completes. Use for lightweight, critical handlers only. |
External Service Event Delivery
For EXTERNAL_SERVICE extensions, the platform delivers events as HTTP POST:
POST {serviceBaseUrl}/events
Content-Type: application/json
{
"eventKey": "crm.deal.closed",
"schemaVersion": "1.0",
"tenantId": "ws-uuid",
"requestId": "req-uuid",
"actor": "user-uuid",
"occurredAt": "2026-04-13T01:00:00Z",
"payload": {
"dealId": "deal-123",
"amount": "5000.00"
}
}Return 200 OK to acknowledge. Failed deliveries are stored in dead_letter_event for replay.