Skip to main content
v2

Extension Points and Intervention Scenarios

EngineStepHook - intervene between any steps

Real scenario: For LOG_ANALYSIS, inject additional retrieval hint before schema extraction.

EngineStepHook example
package: com.zapper.convengine.hooks
JAVA
@Component
public class LogAnalysisHintHook implements EngineStepHook {
@Override
public boolean supports(EngineStep.Name stepName, EngineSession session) {
return EngineStep.Name.SchemaExtractionStep == stepName
&& "LOG_ANALYSIS".equalsIgnoreCase(session.getIntent());
}

@Override
public void beforeStep(EngineStep.Name stepName, EngineSession session) {
session.putInputParam("log_source_priority", "APM_FIRST");
}
}
Where to use this

Use hooks when you need low-friction runtime intervention without forking framework steps.

ContainerDataTransformer - reshape container response

Real scenario: CCF returns nested payload; you flatten to schema-friendly map.

ContainerDataTransformer example
package: com.zapper.convengine.transformers
JAVA
@Component
@ContainerDataTransformer(intent = "REQUEST_TRACKER", state = "IDLE")
public class RequestTrackerContainerTransformer implements ContainerDataTransformerHandler {
@Override
public Map<String, Object> transform(ContainerComponentResponse response,
EngineSession session,
Map<String, Object> inputParams) {
Map<String, Object> out = new LinkedHashMap<>();
out.put("ticket_id", inputParams.get("ticketId"));
out.put("status", "IN_REVIEW");
return out;
}
}

ResponseTransformer - post-process final payload

Real scenario: Add support-team escalation footer for high-severity disconnect failures.

ResponseTransformer example
package: com.zapper.convengine.transformers
JAVA
@Component
@ResponseTransformer(intent = "DISCONNECT_ELECTRICITY", state = "FAILED")
public class DisconnectFailureResponseTransformer implements ResponseTransformerHandler {
@Override
public OutputPayload transform(OutputPayload responsePayload,
EngineSession session,
Map<String, Object> inputParams) {
if (responsePayload instanceof TextPayload(String text)) {
return new TextPayload(text + "\\nIf this is urgent, call support at +1-800-000-0000.");
}
return responsePayload;
}
}

ContainerDataInterceptor - intercept request/response around CCF

Real scenario: add tenant metadata and redact a field before persistence.

Interceptor scenario (concept)
TEXT
Before Execute:
- inject tenantId/requestId
- attach observability headers

After Execute:
- redact sensitive node from raw container payload
- enrich session input params for downstream rule checks

ConvEngineVerboseAdapter - publish UI verbose messages from consumer code

Use this when hooks, transformers, or custom Spring beans need to emit progress/error messages to the UI without embedding transport code.

Verbose adapter example
package: com.zapper.convengine.hooks
JAVA
@Component
@RequiredArgsConstructor
public class LoanConfirmationHook implements EngineStepHook {

private final ConvEngineVerboseAdapter verboseAdapter;

@Override
public void beforeStep(EngineStep.Name stepName, EngineSession session) {
verboseAdapter.publish(session, this, "PRECHECK_STARTED");

verboseAdapter.publishText(
session,
this,
"PRECHECK_NOTE",
"Reviewing customer [[\${context.customerId}]] in state [[\${state}]]."
);
}
}

What it gives you:

  • publish(session, this, "TOKEN"): DB-backed ce_verbose lookup using this.getClass().getSimpleName()
  • publish(session, this, "TOKEN", metadata): same, with extra metadata for resolver matching and rendering
  • publishError(...): same path, error branch
  • publishText(...): bypasses ce_verbose row lookup and sends direct text, still rendered through Thymeleaf

Recommended pattern:

  • use publish(...) when the wording should be controlled by ce_verbose
  • use publishText(...) when the wording should come directly from consumer Java

MCP extension SPI map (consumer-overridable)

MCP and runtime extension points

ExtensionWhere it runsTypical consumer use
`LlmClient` (embedding implementation)`db.semantic.interpret` + `db.semantic.query` retrieval pathsProvide your own embedding provider/model for semantic retrieval and failure-memory ranking.
`PostgresQueryInterceptor``postgres.query` pre-guardrail execution pathNormalize/repair SQL, enforce schema prefixes, tenant constraints, org SQL policy.
`DbToolHandler`MCP DB tool execution pathRegister new DB-backed MCP tool_code handlers in Java.
`HttpApiRequestingToolHandler` / `HttpApiProcessorToolHandler`MCP HTTP tool pathAdd custom request/response mapping logic for HTTP tools.
`MessageResolver`verbose message resolutionCustom verbose token mapping and templated payload behavior.

LlmClient embedding override for semantic retrieval

Real scenario: replace framework embedding calls with your provider for semantic concept retrieval and failure-example ranking.

LlmClient embedding override example
package: com.zapper.convengine.mcp
JAVA
@Component
public class CustomLlmClient implements LlmClient {
@Override
public float[] generateEmbedding(Object session, String text) {
// Call your embedding model/provider and return float[]
return new float[] {0.1f, 0.2f, 0.3f};
}
}

PostgresQueryInterceptor - normalize/fix SQL before execution

Real scenario: planner generated epoch conversion for a timestamp column (to_timestamp(ts_col/1000.0)), causing Postgres error.
Interceptor rewrites SQL before McpSqlGuardrail and execution.

PostgresQueryInterceptor example
package: com.zapper.convengine.mcp
JAVA
@Component
@Order(10) // runs before framework default interceptor
public class PostgresTimestampFixInterceptor implements PostgresQueryInterceptor {

@Override
public boolean supports(CeMcpTool tool, EngineSession session, Map<String, Object> args) {
return tool != null && "postgres.query".equalsIgnoreCase(tool.getToolCode());
}

@Override
public String intercept(String sql, CeMcpTool tool, EngineSession session, Map<String, Object> args) {
if (sql == null || sql.isBlank()) return sql;
// sample targeted rewrite policy
return sql.replaceAll("(?i)to_timestamp\\\\(\\\\s*r\\\\.requested_at\\\\s*/\\\\s*1000(?:\\\\.0+)?\\\\s*\\\\)", "r.requested_at")
.replaceAll("(?i)to_timestamp\\\\(\\\\s*b\\\\.updated_at\\\\s*/\\\\s*1000(?:\\\\.0+)?\\\\s*\\\\)", "b.updated_at");
}
}
Order and fallback

Framework provides DefaultPostgresQueryInterceptor with lowest precedence.
Define your interceptor bean with higher @Order to override/augment behavior.

Additional framework extension interfaces

Other consumer extension interfaces available

InterfacePurpose
`IntentResolver`Provide custom intent resolution strategy.
`IntentCollisionResolver`Resolve ties when multiple intents match.
`RuleActionResolver`Add custom rule action execution logic.
`RuleTypeResolver`Add custom rule match/evaluation type.
`OutputFormatResolver`Add/override output format handling.
`ResponseTypeResolver`Add/override response type handling.
`ConversationHistoryProvider`Customize conversation history loading source.
`ConvEngineSchemaResolver`Customize schema extraction and mapping behavior.

Rule Action Playbook (SET_TASK, SET_JSON, GET_CONTEXT, GET_SESSION, SET_INPUT_PARAM, SET_DIALOGUE_ACT)

These actions execute inside RulesStep.execute(...) and mutate the live EngineSession.

Set ce_rule.phase based on where you want the action to run:

  • POST_DIALOGUE_ACT: immediately after DialogueActStep, before InteractionPolicyStep.
  • PRE_RESPONSE_RESOLUTION: normal RulesStep pass.
  • POST_AGENT_INTENT: post-intent pass inside AgentIntentResolver.

Action value format (exact runtime behavior)

Actionaction_value formatEngine behavior
SET_TASKbeanName:methodName or beanName:methodA,methodBInvokes Spring bean methods via CeRuleTaskExecutor
SET_JSONtargetKey:jsonPathExtracts JSONPath from session eject and stores into inputParams[targetKey]
GET_CONTEXTtargetKey (optional)Stores session.contextDict() into inputParams[targetKey] (default key=context)
GET_SESSIONtargetKey (optional)Stores session.sessionDict() into inputParams[targetKey] (default key=session)
SET_INPUT_PARAM{"key":value,...}Writes literal runtime flags directly into inputParams
SET_DIALOGUE_ACT"EDIT" or {"dialogueAct":"EDIT",...}Overrides final dialogue_act / confidence / source and can sync standalone_query

SET_TASK - execute consumer Java methods from rule

Use this when a rule match must trigger consumer-side business logic (incident raise, tracker lookup, eligibility fetch, etc).

1

Create ce_rule row

Set ce_rule.action to SET_TASK and provide bean/method mapping in action_value.

SET_TASK rule example (SQL)
SQL
INSERT INTO ce_rule (phase, intent_code, state_code, rule_type, match_pattern, action, action_value, priority, enabled, description) VALUES ('PRE_RESPONSE_RESOLUTION', 'REQUEST_TRACKER', 'ANY', 'REGEX', '(?i).*track.*request.*', 'SET_TASK', 'requestTrackerTask:loadStatus,attachEta', 10, true, 'Load tracker status + ETA from consumer service');
2

Implement task bean

Bean must be a Spring bean with the configured bean name and implement CeRuleTask. Methods are invoked with (EngineSession session, CeRule rule).

Consumer task bean (Java)
package: com.zapper.convengine.tasksfile: src/main/java/com/acme/convengine/tasks/RequestTrackerTask.java
JAVA
@Component("requestTrackerTask")
public class RequestTrackerTask implements CeRuleTask {

public void loadStatus(EngineSession session, CeRule rule) {
String requestId = String.valueOf(session.getInputParams().getOrDefault("requestId", ""));
// fetch from your DB/service
session.putInputParam("requestStatus", "APPROVAL_PENDING");
session.putInputParam("lastUpdated", "2026-02-10T11:40:00Z");
}

public void attachEta(EngineSession session, CeRule rule) {
session.putInputParam("eta_hours", 12);
}
}
3

Use in response generation

Downstream prompt/response can read new context/inputParams (for example requestStatus, lastUpdated, eta_hours).

SET_JSON - move JSONPath value into input params

Use this to extract one value from runtime session JSON into a flat prompt var.

SET_JSON rule example (SQL)
SQL
INSERT INTO ce_rule (phase, intent_code, state_code, rule_type, match_pattern, action, action_value, priority, enabled, description) VALUES ('PRE_RESPONSE_RESOLUTION', 'LOG_ANALYSIS', 'ANY', 'JSON_PATH', '$.schemaJson.errorCode != null', 'SET_JSON', 'error_code:$.schemaJson.errorCode', 20, true, 'Expose extracted errorCode as prompt var');
What exactly gets updated

SET_JSON writes to session.putInputParam("error_code", value).
It does not change session.contextJson unless your subsequent task/hook updates context explicitly.

GET_CONTEXT - snapshot context into input params

Use this when prompt templates or tasks need full context as a single variable.

GET_CONTEXT rule example (SQL)
SQL
INSERT INTO ce_rule (phase, intent_code, state_code, rule_type, match_pattern, action, action_value, priority, enabled, description) VALUES ('PRE_RESPONSE_RESOLUTION', 'REQUEST_TRACKER', 'ANY', 'REGEX', '(?i).*status.*', 'GET_CONTEXT', 'ctx_snapshot', 30, true, 'Expose full context to prompt/task layer');

If action_value is blank, engine uses default key context.

GET_SESSION - snapshot session facts into input params

Use this for advanced derived responses that need runtime flags (schemaComplete, intentLocked, missingRequiredFields, etc).

GET_SESSION rule example (SQL)
SQL
INSERT INTO ce_rule (phase, intent_code, state_code, rule_type, match_pattern, action, action_value, priority, enabled, description) VALUES ('PRE_RESPONSE_RESOLUTION', 'DISCONNECT_ELECTRICITY', 'ANY', 'JSON_PATH', '$.schemaComplete == false', 'GET_SESSION', 'session_snapshot', 40, true, 'Expose full runtime session facts for follow-up prompt decisions');

If action_value is blank, engine uses default key session.

SET_INPUT_PARAM - write runtime flags directly

Use this when you need lightweight runtime variables for later rules/steps without creating a custom task bean.

SET_INPUT_PARAM rule example (SQL)
SQL
INSERT INTO ce_rule (phase, intent_code, state_code, rule_type, match_pattern, action, action_value, priority, enabled, description) VALUES ('POST_SCHEMA_EXTRACTION', 'LOAN_APPLICATION', 'ELIGIBILITY_GATE', 'JSON_PATH', '$[?(@.schemaComplete == true)]', 'SET_INPUT_PARAM', '{"awaiting_confirmation":true,"confirmation_key":"LOAN_APPLICATION_CONFIRM"}', 11, true, 'Mark confirmation flags after schema becomes complete');

Typical uses:

  • awaiting_confirmation
  • confirmation_key
  • skip_schema_extraction
  • routing_decision
  • correction_applied

This is the clean way to make later ce_rule phases react to:

  • confirmation answers (AFFIRM / NEGATE)
  • correction routing
  • post-schema, pre-MCP gating

SET_DIALOGUE_ACT - override final dialogue-act after classification

Use this when DialogueActStep correctly preserves an LLM candidate (for example dialogue_act_llm_candidate=EDIT) but the final guarded result should be overridden before InteractionPolicyStep.

This is especially useful in POST_DIALOGUE_ACT.

SET_DIALOGUE_ACT rule example (SQL)
SQL
INSERT INTO ce_rule (phase, intent_code, state_code, rule_type, match_pattern, action, action_value, priority, enabled, description) VALUES ('POST_DIALOGUE_ACT', 'LOAN_APPLICATION', 'ANY', 'JSON_PATH', '$[?(@.inputParams.dialogue_act == ''NEW_REQUEST'' && @.inputParams.dialogue_act_source == ''REGEX_GUARD'' && @.inputParams.dialogue_act_llm_candidate == ''EDIT'')]', 'SET_DIALOGUE_ACT', '{"dialogueAct":"EDIT","source":"POST_DIALOGUE_ACT_RULE"}', 5, true, 'Promote guarded LLM EDIT back to EDIT before interaction policy');

Accepted action_value formats:

  • simple string: EDIT
  • structured JSON: {"dialogueAct":"EDIT","confidence":0.99,"source":"POST_DIALOGUE_ACT_RULE","standaloneQuery":"..."}

SET_DIALOGUE_ACT updates:

  • dialogue_act
  • dialogue_act_confidence
  • dialogue_act_source
  • optional standalone_query (which also refreshes resolved_user_input)
Operational guardrails

SET_TASK methods run during rule execution. Keep methods deterministic and idempotent.
For side-effecting calls (ticket creation, webhook dispatch), guard with strict rule conditions and add idempotency keys from conversationId.

Where to trace this in code
Resolvers
engine/rule/type/provider/SetTaskActionResolver.javaengine/rule/type/provider/SetJsonActionResolver.javaengine/rule/type/provider/GetContextActionResolver.javaengine/rule/type/provider/GetSessionActionResolver.javaengine/rule/type/provider/SetDialogueActActionResolver.java
Task invocation
engine/rule/task/CeRuleTaskExecutor.java
Execution loop
engine/steps/RulesStep.java

Custom action resolver (RuleActionResolver)

Consumer can define a brand-new rule action without changing framework core.

1

Create custom resolver bean

Implement RuleActionResolver and return your action name from action().

Custom RuleActionResolver example
package: com.zapper.convengine.rulesfile: src/main/java/com/acme/convengine/rules/EnrichCustomerTierActionResolver.java
JAVA
@Component
public class EnrichCustomerTierActionResolver implements RuleActionResolver {

@Override
public String action() {
return "ENRICH_TIER";
}

@Override
public void resolve(EngineSession session, CeRule rule) {
// action_value example: customerTier:PREMIUM
String raw = rule.getActionValue() == null ? "" : rule.getActionValue();
String[] parts = raw.split(":", 2);
String key = parts.length > 0 && !parts[0].isBlank() ? parts[0] : "customerTier";
String value = parts.length > 1 ? parts[1] : "STANDARD";

session.putInputParam(key, value);
}
}
2

Use action in ce_rule

Set ce_rule.action to your custom token (case-insensitive lookup), for example ENRICH_TIER.

ce_rule row for custom action
SQL
INSERT INTO ce_rule (phase, intent_code, rule_type, match_pattern, action, action_value, priority, enabled, description) VALUES ('PRE_RESPONSE_RESOLUTION', 'REQUEST_TRACKER', 'REGEX', '(?i).*vip.*', 'ENRICH_TIER', 'customerTier:PREMIUM', 5, true, 'Mark VIP tier for downstream response logic');
Auto-registration behavior

No manual factory config is needed. RuleActionResolverFactory auto-registration behavior auto-discovers all Spring beans implementing RuleActionResolver and maps by action().toUpperCase().