GoodBooks GB5 — EIP & DirectAction Developer Reference

Architecture internals, code examples, DB schemas, and QC test scenarios for the Enterprise Integration Platform (EIP) and DirectAction email-action system.

System Overview

The GB5 backend provides two complementary integration platforms for external actors to interact with the ERP:

PlatformTriggerChannelUse Case
EIP Conversation EngineInbound text messageWhatsApp, Slack, Teams, Telegram, SMS, PostmanMulti-turn guided workflows (leave requests, order queries, HR bots)
DirectActionClick a link / buttonEmail link, WhatsApp interactive buttonOne-click approval / rejection / return on an existing entity (workflow task, PO, leave)

Both platforms share the Action Pipeline (TEVENTACTIONRUN → TACTIONOUTBOX → Dapr pub/sub → EmailActionHandler) for outbound notification delivery.

Architecture Map

External Channel (WhatsApp / Email / Slack) │ ▼ ┌─────────────────────────────────────────────────────────┐ │ FrameworkSL │ │ POST /EIPConversation/Receive (EIP) │ │ POST /MessageHubGenerator/Request... (WhatsApp WH) │ │ GET /Action/Execute (DirectAction) │ │ POST /Action/Submit (DA + remarks) │ └───────────────┬────────────────────────┬────────────────┘ │ │ ▼ ▼ EIPConversationBLL GenericApiDirectActionHandler │ │ ┌─────────┼──────────┐ │ ▼ ▼ ▼ ▼ EIPFlowEngine SessionDAL ActionEngine HTTP call to internal API │ │ ▼ ▼ EIPResponseEngine TEVENTACTIONRUN / TACTIONOUTBOX │ │ ▼ ▼ Channel Handler Dapr pub/sub → EmailActionHandler → SMTP (WhatsApp/Slack/etc.) → DirectAction tokens in email

EIP Conversational Engine

Every inbound message goes through a 6-phase sequential pipeline. Each phase produces a result consumed by the next. If any phase fails, the request is terminated with an error response.

Phase 1
Normalization
Phase 2
Routing
Phase 3
Flow Engine
Phase 4
Capability Engine
Phase 5
Action Engine
Phase 6
Response Engine

The orchestrator is EIPConversationEngine.ProcessIncomingMessageAsync(). All phases receive the same EIPExecutionContextDTO, which accumulates state across phases.

Phase 1 — Channel Normalization

File: FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ChannelNormalizer/EIPChannelNormalizer.cs

Converts vendor-specific webhook payloads into the internal EIPExecutionContextDTO.

Input fieldSource DTONotes
TenantIdEIPConversationDTO.TenantIdRequired; string cast to int
ChannelTypeEIPConversationDTO.ChannelTypeEnum — see below
UserIdentifierEIPConversationDTO.UserIdentifierPhone number, email, or user ID
NormalizedMessageEIPConversationDTO.Message (trimmed/lower)Used for routing keyword match
FlowCodeEIPConversationDTO.FlowCodeOptional; bypasses routing if set
CorrelationIdGenerated GuidPresent in all log lines for tracing
Location auto-injection
If EIPConversationDTO.Location is present, Phase 1 also sets context.Variables["_Location.Lat"], "_Location.Lng", and "_Location.Address" automatically before the flow engine runs.

EIPChannelType enum

ValueNameWebhook Provider
0PostManDirect API call (testing)
1WhatsAppMeta / Celitix
2TeamsMicrosoft Teams
3TelegramTelegram Bot API
4SlackSlack Webhook
5SMSSMS gateway
6AppChatAppChatChannelHandler — no-op delivery; messages delivered via EIPChatHub SignalR on /hubs/eip-chat; all rich fields in EIPChatMessageDTO
7VoiceVoiceChannelHandler — returns AdditionalData { TtsText, SsmlText, IsCompleted }; no outbound HTTP call

Phase 2 — Routing Engine

File: FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/RoutingEngine/EIPRoutingEngine.cs

Loads active rules from MEIPROUTINGRULE, ordered by PRIORITY ascending (lower number = higher priority). Rules are matched in priority order; first match wins.

MATCHTYPEOperatorExample MATCHVALUEMatches
1EQUALSorder status"order status" exactly
2STARTS_WITHorder"order 1234", "order help"
3CONTAINSapprove"please approve", "need to approve this"
4REGEX^PO-\d{4,}$"PO-1234", "PO-99999"
Routing fallback
If EIPConversationDTO.FlowCode is set by the caller, Phase 2 is bypassed and the supplied FlowCode is used directly. This is the mechanism for WhatsApp flows that know their target conversation.
No default route
If no routing rule matches and no FlowCode was supplied, the engine throws an exception and returns an error response. Always ensure a catch-all rule (e.g., CONTAINS with pattern ".*" at lowest priority).

Phase 3 — Flow Engine

File: FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/FlowEngine/EIPFlowEngine.cs

Executes the flow definition loaded from MEIPFLOWDEFINITION.JSONDEFINITION. The engine loops through steps (max 50 iterations — prevents infinite loops) dispatching to registered step handlers.

Flow Definition JSON Structure

{
  "FlowCode": "LEAVE_REQUEST",
  "StartStepCode": "GREETING",
  "Steps": {
    "GREETING": {
      "StepType": "MESSAGE",
      "MessageTemplate": "Hello {CustomerName}! I can help with your leave request. Type 'start' to begin.",
      "NextStepCode": "COLLECT_DATES"
    },
    "COLLECT_DATES": {
      "StepType": "INPUT",
      "MessageTemplate": "Please enter your leave start date (YYYY-MM-DD):",
      "ContextKey": "LeaveStartDate",
      "Validation": { "Required": true, "Pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
      "NextStepCode": "COLLECT_REASON"
    },
    "COLLECT_REASON": {
      "StepType": "CHOICE",
      "MessageTemplate": "What type of leave is this?",
      "Choices": {
        "1. Annual Leave":  "CONFIRM_ANNUAL",
        "2. Sick Leave":    "CONFIRM_SICK",
        "3. Unpaid Leave":  "CONFIRM_UNPAID"
      }
    },
    "CONFIRM_ANNUAL": {
      "StepType": "CONDITION",
      "ConditionExpression": "confirm",
      "NextStepCode": "SUBMIT_LEAVE"
    },
    "SUBMIT_LEAVE": {
      "StepType": "ACTION",
      "ActionType": "CALL_API",
      "ApiConfig": {
        "Endpoint": "/Leave/SaveLeave",
        "Method": "POST",
        "Body": "{\"LeaveType\": 1, \"StartDate\": \"{LeaveStartDate}\", \"UserId\": \"{UserId}\"}"
      },
      "ContextKey": "SubmitResult",
      "NextStepCode": "DONE"
    },
    "DONE": {
      "StepType": "END",
      "MessageTemplate": "Your leave has been submitted successfully! Reference: {SubmitResult}"
    }
  }
}

Execution Rules

All Step Types

StepTypePauses?Key FieldsPurpose
MESSAGE / PROMPT Yes MessageTemplate, NextStepCode Display text to user. Waits for any reply to proceed.
INPUT Yes MessageTemplate, ContextKey, Validation (Required, Pattern) Prompt for free-form input. Validates with regex if Pattern set. Stores reply in context.Variables[ContextKey].
CHOICE Yes MessageTemplate, Choices (dict: label → NextStepCode) Present numbered menu. User reply is matched against keys; matching key's NextStepCode is followed.
CONDITION No ConditionExpression (keyword), NextStepCode Legacy: checks if NormalizedMessage contains ConditionExpression. Moves to NextStepCode if true.
CONDITIONAL No ConditionKey, Conditions[] (operator/value/NextStepCode), DefaultNextStepCode Multi-branch: evaluates context variable against each condition operator in order. Falls through to DefaultNextStepCode.
ACTION No ActionType (CALL_API / TRIGGER_MESSAGEHUB / DB_UPDATE), ApiConfig, ContextKey Executes a system operation. Stores result in context.Variables[ContextKey].
CAPABILITY No CapabilityCode, MessageTemplate Invokes EIPCapabilityEngine for domain-specific business logic with optional risk/OTP gating.
EXTERNAL_ACTION No DecisionMap (decision → NextStepCode) Legacy backward-compat: maps incoming button/decision value to next step code.
END — MessageTemplate Sends final message, marks flow complete, expires session.
QUALIFY No QualifyConfig (BoundToType, InputKeys[], Stage, OutputFactKey, MapperCode, OutputMapKey, OnValidationErrorStepCode) Runs IQualifierFacadeImpl.ExecuteStageAsync; facts stored as JSON in OutputFactKey. Error findings route to OnValidationErrorStepCode and set _QualifyError variable.
ENRICH No EnrichConfig (MapperCode, InputKey, OutputKey) Runs IMappingEngineBLL.TransformAsync directly; result stored in context.Variables[OutputKey].
DYNAMIC_CHOICE Yes (first visit) DynamicChoiceConfig (SourceKey, DataPath, LabelField, ValueField, StoreSelectedKey, StoreItemKey, MaxChoices) Builds buttons from a JSON array in context.Variables[SourceKey]. First visit pauses and returns buttons. On selection, stores chosen value in StoreSelectedKey and full item JSON in StoreItemKey.
LOCATION Yes (awaiting location) ContextKey Pauses until _Location.Lat is set in Variables (by Phase 1 normalizer on the next inbound message). Stores {"Lat":…,"Lng":…,"Address":…} JSON in ContextKey; sets RequestLocation=true on the response.
LINK No LinkButtons[] (Title, Url), ButtonType (REPLY / URL / CALL / QR) Emits one or more link/action buttons to the user; routes immediately to NextStepCode without pausing.
PRESENT No ContextKey, ResponseFormat (Type, Fields[], MaxRows, TitleField) Parses JSON from context.Variables[ContextKey] and produces an EIPStructuredDataDTO. AppChat renders natively; WhatsApp/SMS receive plain-text via EIPTextFormatter.

CONDITIONAL Step — Operators

OperatorMeaning
EXISTScontext.Variables[ConditionKey] is not null/empty
NOT_EXISTScontext.Variables[ConditionKey] is null or empty
EQUALSexact string match (case-insensitive)
NOT_EQUALSstring mismatch
INvalue is in comma-separated list
GTnumeric greater-than
LTnumeric less-than

Example: CONDITIONAL Step

{
  "StepType": "CONDITIONAL",
  "ConditionKey": "RiskScore",
  "Conditions": [
    { "Operator": "GT", "Value": "80", "NextStepCode": "HIGH_RISK_BLOCK" },
    { "Operator": "GT", "Value": "50", "NextStepCode": "MEDIUM_RISK_OTP" }
  ],
  "DefaultNextStepCode": "LOW_RISK_PROCEED"
}

Phase 4 — Capability Engine

File: FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/CapabilityEngine/EIPCapabilityEngine.cs

Only invoked for CAPABILITY steps. Sequence:

  1. Risk Assessment — EIPRiskAssessment.EvaluateRiskAsync(). Returns risk level + RequiresOtp flag. Currently: keyword "PAYMENT" → HIGH risk.
  2. OTP Generation (if high risk) — EIPOtpService.GenerateOtpAsync(). Currently logs a 6-digit code only; no DB persistence yet.
  3. Action Context Build — wraps CapabilityCode + context variables into EIPActionContextDTO with IdempotencyKey = CorrelationId.
  4. Action Executor — routes to IEIPActionExecutor.ExecuteAsync().
Known limitation
OTP is generated and logged but not persisted to DB, and no verification step exists. CAPABILITY steps with risk assessment are production-visible but OTP delivery is a stub only.

Phase 5 — Action Engine

File: FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ActionEngine/EIPActionEngine.cs

Handler factory: case-insensitive dictionary of IEIPActionHandler keyed by ActionCode.

ActionCodeHandlerWhat it does
CRUD_OPERATION CrudOperationHandler HTTP POST/PUT/GET/DELETE to an internal GB5 endpoint. Payload contains Endpoint, Method, Body. Result stored in context.Variables[ContextKey].
CALL_EXTERNAL_API IntegrationActionHandler HTTP call to a 3rd-party URL. URL, headers, method, body all configured in the step's ApiConfig.
SEND_NOTIFICATION NotificationActionHandler Inserts TEVENTACTIONRUN + TACTIONOUTBOX rows. Background dispatcher publishes to Dapr topic action-exec-p{tenantId%5}. EmailActionHandler picks up and sends email.
VALIDATION_ACTION ValidationActionHandler Stub — validates context data against configured rules.

CRUD_OPERATION Payload Example

// Flow step ApiConfig:
{
  "Endpoint": "/Leave/SaveLeave",
  "Method": "POST",
  "Body": "{\"LeaveType\": 1, \"StartDate\": \"{LeaveStartDate}\"}"
}

// Handler resolves:
// SysJobSettings:ServiceBaseUrl + Endpoint = full URL
// Variables already substituted before handler call

Phase 6 — Response Engine

File: FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ResponseEngine/EIPResponseEngine.cs

Sequence:

  1. Rate limit check — in-memory ConcurrentDictionary keyed "CHANNEL:RECIPIENT". 2-second window per recipient. Drops message silently if too frequent (protects downstream providers).
  2. Handler selection — looks up IEIPChannelHandler by channel name (case-insensitive). Throws if no handler registered.
  3. Delivery — calls handler.SendAsync(context, login, ct).
  4. Metadata update — records last sent time, recipient, channel, response status on context for audit.

Response Fields

The following fields are present on EIPFlowExecutionResultDTO, EIPExecutionResultDTO, and (where applicable) EIPChatMessageDTO to support extended channel capabilities introduced by the PRESENT, LOCATION, LINK, and Voice step types.

FieldTypeDescription
StructuredData EIPStructuredDataDTO? TABLE / CARD / LIST result produced by a PRESENT step. AppChat renders natively; WhatsApp and SMS receive a plain-text fallback via EIPTextFormatter.
QrCodeData string? Raw QR content string. AppChat renders a native canvas QR code; WhatsApp sends it as an image message type; Telegram sends it via sendPhoto.
InputHint string? Widget hint for the AppChat input control. Valid values: text, number, date, email, phone, qr_scan, file, rating.
RatingMax int? Maximum star value for the rating widget (default 5). Only relevant when InputHint = "rating".
RequestLocation bool Signals the channel to show a location-share UI. WhatsApp sends a prompt; Telegram sends a ReplyKeyboardMarkup; AppChat activates HTML5 Geolocation.
VoiceHint string? SSML markup string used by the Voice channel handler as SsmlText in AdditionalData. Ignored by all other channel handlers.
EIPChannelResponseDTO — AdditionalData
EIPChannelResponseDTO now carries a Dictionary<string, object>? AdditionalData property. The VoiceChannelHandler uses this to return { "TtsText": "...", "SsmlText": "...", "IsCompleted": true/false } without making any outbound HTTP delivery call.

Session State

Table: TEIUSERSESSION  |  DAL: EIPSessionDAL

Sessions enable multi-turn conversations. The engine restores session before Phase 2, upserts after Phase 3 if paused, and expires on flow completion.

ColumnTypePurpose
SESSIONIDuniqueidentifier PKInternal session handle
USERIDENTIFIERnvarchar(255)Phone / email / user ID — identifies who the session belongs to
TENANTIDintMulti-tenant isolation
CHANNELTYPEintEnum value (0–7) — WhatsApp session ≠ Teams session for same user
FLOWCODEnvarchar(100)Which flow this session is in
CURRENTSTEPCODEnvarchar(100)Step to resume at on next message
CONTEXTJSONnvarchar(max)JSON dict of all accumulated Variables
STARTEDATdatetime2When conversation started
LASTACTIVITYATdatetime2Updated on every message — used for 30-min TTL check
STATUSint1 = active, 0 = expired

Session Lifecycle

GET active session (LASTACTIVITYAT > NOW - 30min, STATUS=1) │ ├─ Found → Restore: FlowCode, CurrentStepCode, Variables (JSON→Dict) │ └─ Not found → New session: use routed FlowCode, start at StartStepCode Execute Phase 3 Flow Engine │ ├─ Flow paused (waiting for user input) │ └─ MERGE TEIUSERSESSION with new CurrentStepCode + ContextJson │ └─ Flow completed (END step or null NextStepCode) └─ UPDATE TEIUSERSESSION SET STATUS=0

Multi-Turn Conversation Example

This traces a 3-turn leave request conversation, showing exact DB state at each turn.

Turn 1 — User sends "start leave"

POST /EIPConversation/Receive
{ "message": "start leave", "tenantId": "42", "channelType": 1,
  "userIdentifier": "+9715551234" }

Phase 2: MEIPROUTINGRULE matches STARTS_WITH "start" → FlowCode="LEAVE_REQUEST"
Phase 3: Starts at "GREETING" (MESSAGE step) → pauses
Session saved: CurrentStepCode="COLLECT_DATES", FlowCode="LEAVE_REQUEST",
               ContextJson="{}"

Response: "Hello! I can help with your leave request..."

Turn 2 — User sends "2024-07-15"

POST /EIPConversation/Receive
{ "message": "2024-07-15", "tenantId": "42", "channelType": 1,
  "userIdentifier": "+9715551234" }

Session restored: CurrentStepCode="COLLECT_DATES"
Phase 3: INPUT step validates "2024-07-15" against pattern "^\d{4}-\d{2}-\d{2}$" → valid
         Stores Variables["LeaveStartDate"] = "2024-07-15"
         Moves to COLLECT_REASON (CHOICE step) → pauses
Session saved: CurrentStepCode="COLLECT_REASON",
               ContextJson="{\"LeaveStartDate\":\"2024-07-15\"}"

Response: "What type of leave? 1.Annual 2.Sick 3.Unpaid"

Turn 3 — User sends "1"

POST /EIPConversation/Receive
{ "message": "1", "tenantId": "42", "channelType": 1,
  "userIdentifier": "+9715551234" }

Session restored: CurrentStepCode="COLLECT_REASON", Variables={"LeaveStartDate":"2024-07-15"}
Phase 3: CHOICE "1" matches "1. Annual Leave" → NextStepCode="CONFIRM_ANNUAL"
         CONFIRM_ANNUAL (ACTION/CALL_API) → POST /Leave/SaveLeave (no pause)
         Returns result → DONE (END step) → sends final message
Session expired: STATUS=0

Response: "Your leave has been submitted! Reference: LV-2024-001"

Channel Handlers

ChannelHandler ClassMessage Types Supported
POSTMANPostmanChannelHandlerText (echo/logging only — testing)
WHATSAPPWhatsAppChannelHandlerText, Media (image/video/doc), Interactive buttons, Template messages. Dual provider: Celitix or Meta Graph API v18. QR image (image type via qrserver.com link), RequestLocation plain-text prompt, EIPTextFormatter plain-text fallback for StructuredData.
TEAMSTeamsChannelHandlerAdaptive cards, text
SLACKSlackChannelHandlerBlock kit messages, text
TELEGRAMTelegramChannelHandlerText, inline keyboards. QR photo via sendPhoto API, RequestLocation via ReplyKeyboardMarkup { request_location: true }.
SMSSmsChannelHandlerPlain text
APPCHATAppChatChannelHandlerNo-op HTTP delivery — message delivered via EIPChatHub SignalR on /hubs/eip-chat. All rich fields (StructuredData, QrCodeData, InputHint, RatingMax, RequestLocation) forwarded in EIPChatMessageDTO.
VOICEVoiceChannelHandlerNo HTTP delivery. Returns AdditionalData { TtsText, SsmlText, IsCompleted }; VoiceHint used as SsmlText. Caller reads result from EIPChannelResponseDTO.AdditionalData.

WhatsApp — Retry Policy

Polly exponential backoff: 3 retries with delays of 2s, 4s, 8s. Dual provider selection via MessageHubSettings:Celitix:Enable and MessageHubSettings:WhatsApp:Enable in appsettings.

Qualifier & Mapper Integration

QUALIFY Step

The QUALIFY step calls IQualifierFacadeImpl.ExecuteStageAsync() to run a named qualifier stage against input data assembled from context variables.

Execution Context Built by the Step Handler

var ctx = new QualifierExecutionContext
{
    ClientId      = login.ClientId,
    EntityId      = qualifyConfig.EntityId,           // optional numeric entity PK
    EntityCode    = qualifyConfig.BoundToType,         // qualifier bound-to type code
    Stage         = qualifyConfig.Stage,               // e.g., "VALIDATE_ADDRESS"
    Scope         = qualifyConfig.Scope,               // optional scope tag
    Document      = JsonDocument.Parse(inputsJson),    // JSON built from InputKeys[]
    CorrelationId = context.CorrelationId
};

Result Handling

ENRICH Step

The ENRICH step calls IMappingEngineBLL.TransformAsync() directly, without a qualifier stage:

var transformed = await _mappingEngine.TransformAsync(
    enrichConfig.MapperCode,
    context.Variables[enrichConfig.InputKey],
    login,
    ct);
context.Variables[enrichConfig.OutputKey] = transformed;

Dependency Injection

Auto-registration — no manual entries needed
Both IQualifierFacadeImpl → QualifierFacadeImpl and IMappingEngineBLL → MappingEngineBLL are auto-registered by assembly scan at FrameworkSL/Program.cs:544. No manual services.AddScoped() entries are required when adding new qualifier or mapper implementations.

Structured Data & PRESENT Step

EIPStructuredDataDTO

The PRESENT step parses JSON from a context variable and produces an EIPStructuredDataDTO, which is placed on the flow execution result and forwarded to the channel handler.

FieldTypeDescription
TypestringTABLE, CARD, LIST, or TEXT
FieldsList<EIPFieldDefinitionDTO>Column/field definitions — each has Key and Label
RowsList<Dictionary<string, string>>Data rows — each row is a key→value dict matching the field keys
TotalCountintTotal records available (before MaxRows truncation)
IsTruncatedboolTrue when Rows.Count < TotalCount

EIPResponseFormatDTO — Step Configuration

Defined inside the PRESENT step's flow JSON under ResponseFormat:

FieldTypeDefaultDescription
Typestring—TABLE / CARD / LIST / TEXT
DataPathstring?null (root)Dot-path into the context variable's JSON to the array of rows (e.g., "Data.Items")
FieldsEIPFieldDefinitionDTO[]—Which fields to extract and their display labels
MaxRowsint20Maximum rows to include; sets IsTruncated if source has more
TitleFieldstring?nullFor CARD type: which field to use as the card title / heading

EIPTextFormatter — Plain-Text Fallback

File: FrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/FlowEngine/EIPTextFormatter.cs

EIPTextFormatter.FormatAsText(EIPStructuredDataDTO data) converts structured data to a plain-text string for channels that cannot render native structured output (WhatsApp, SMS):

WhatsApp and SMS channel handlers call EIPTextFormatter.FormatAsText() automatically when context.StructuredData != null. AppChat receives the full EIPStructuredDataDTO object for native rendering.

QR Code & Location

QR Code Generation

When a step sets QrCodeData on the execution result, each channel handler renders it differently:

QR Scan Input

For INPUT steps requiring a barcode or QR scan, set Validation.InputType = "qr_scan" in the step definition. This causes the engine to set InputHint = "qr_scan" on the response. The AppChat widget activates the device camera / barcode scanner; when the user scans, the decoded text is sent back as a regular message and processed by the INPUT step validation.

Inbound Location Data

When the channel delivers a location payload, the inbound EIPConversationDTO.Location field is populated as an EIPLocationDTO:

FieldTypeDescription
LatitudedoubleWGS-84 latitude
LongitudedoubleWGS-84 longitude
Addressstring?Reverse-geocoded address (if provided by channel)
Accuracydouble?Accuracy radius in metres (if provided)
Namestring?Place name (Telegram sends this for venue locations)

Phase 1 (Channel Normalizer) detects a non-null Location and automatically sets context.Variables["_Location.Lat"], "_Location.Lng", and "_Location.Address" before the flow engine runs.

RequestLocation Rendering per Channel

When the engine sets RequestLocation = true on the response (produced by a LOCATION step on first visit), each channel reacts:

DirectAction System

DirectAction enables external actors (users in email or WhatsApp) to trigger backend API calls by clicking a link or button — without logging in. The system is stateless (state is in the signed token), one-time-use, and multi-tenant isolated.

Entity Saved → Event Published → EmailActionHandler │ └─ Generates signed token per button per assignee Embeds token URL in email HTML (##APPROVE_URL## → /Action/Execute?token=...) Sends email to assignee User clicks link │ └─ GET /Action/Execute?token=... Validate signature + expiry One-time-use check (TDIRECTACTIONTOKEN) NeedsInput=0: record → call API → success page NeedsInput=1: show remarks form (do NOT record yet) POST /Action/Submit record + remarks → call API → success page

Token Format & Security

Payload

payload = "{actionCode}:{contextId}:{tenantId}:{assigneeUserId}:{expiresAtTicks}:{databaseName}"
token   = Base64Url(payload) + "." + Base64Url(HMAC-SHA256(payload, secret))

Example payload (plaintext before encoding):

WORKFLOW_APPROVE:5001:42:99:638501234567890123:goodbooks_prod_42

Example signed token:

V09SS0ZMT1dfQVBQUk9WRTo1MDAx....Ojk5OjYzODUwMTIzNA.xK7f2mNq8...
FieldExamplePurpose
actionCodeWORKFLOW_APPROVEMaps to MDIRECTACTIONDETAIL row for config lookup
contextId5001PK of the entity being actioned (e.g., WorkflowTaskId)
tenantId42ClientId for multi-tenant isolation
assigneeUserId99The specific user this token was issued for; becomes LoginDTO.UserId for the API call
expiresAtTicks638501234567890123DateTime.Ticks UTC — used for expiry check. Default: 48 hours from generation.
databaseNamegoodbooks_prod_42Tenant DB name — drives connection routing without appsettings lookup

Security Properties

Validation Order (failfast)

  1. Token is not null/empty
  2. Token contains exactly one . separator
  3. Base64Url decode succeeds
  4. HMAC signature matches (fixed-time compare)
  5. Payload has ≥ 6 fields; fields 2–5 parse as int/long
  6. DateTime.UtcNow <= expiresAt

NeedsInput=0 Flow (Direct Click)

Used for simple one-click actions: Approve, Return, Forward.

GET /Action/Execute?token={signedToken} ActionExecuteEndpoint │ ├─ TryValidate(token) → actionCode, contextId, tenantId, │ assigneeUserId, databaseName, expiresAt │ (fail → "Link Expired or Invalid" HTML page) │ ├─ Reconstruct LoginDTO from token fields │ ├─ SHA256(token) → tokenHash ├─ TDIRECTACTIONTOKEN lookup by tokenHash │ (found + UsedAt != null → "Already Actioned" page) │ (found + UsedAt == null, Status=2 → "Already Actioned" page) │ ├─ GetDetailByActionCodeAsync(actionCode) → config │ config.NeedsInput == 1 → show RemarksFormHtml (STOP here) │ ├─ INSERT TDIRECTACTIONTOKEN (NeedsInput=0: record before API call) │ ExpiresAt = tokenExpiresAt (actual token expiry, NOT UtcNow) │ UsedAt = UtcNow, Status = 1 │ ├─ frameworkBaseUrl = Request.Scheme + "://" + Request.Host ├─ GenericApiDirectActionHandler.ExecuteAsync( │ actionCode, contextId, assigneeUserId, │ remarks=null, login, frameworkBaseUrl) │ → Load MDIRECTACTIONDETAIL │ → Build payload (substitute {ContextId}) │ → POST/PUT to fullUrl with Login header │ → Parse GB5 response (Status=400 → extract GeneralErrors) │ └─ Success → "Action Successful" HTML page Failure → "Error: {message}" HTML page

NeedsInput=1 Flow (With Remarks)

Used when the action requires user-provided text (e.g., rejection reason, approval comment).

GET /Action/Execute?token={signedToken} │ ├─ [Same validation + one-time-use check as NeedsInput=0] │ ├─ GetDetailByActionCodeAsync → config.NeedsInput == 1 │ └─ Return RemarksFormHtml (token in hidden field, remarks textarea) Token is NOT recorded yet — user can reload the form safely. ── User fills form and clicks Submit ────────────────────────────── POST /Action/Submit (application/x-www-form-urlencoded) Body: token={signedToken}&remarks=Approved+due+to+budget ├─ TryValidate(token) → same fields + tokenExpiresAt │ (fail → "Link Expired or Invalid") │ ├─ SHA256(token) → tokenHash ├─ TDIRECTACTIONTOKEN lookup │ (found → "Already Actioned" — prevents double-submission) │ ├─ remarks = req.Remarks?.Trim() │ ├─ INSERT TDIRECTACTIONTOKEN │ ExpiresAt = tokenExpiresAt, UsedAt = UtcNow, Status = 1, │ Remarks = remarks ← stored for audit │ ├─ GenericApiDirectActionHandler.ExecuteAsync( │ actionCode, contextId, assigneeUserId, │ remarks, login, frameworkBaseUrl) │ → PayloadTemplate: {ContextId} + {Remarks} substituted │ → POST to internal API │ └─ Success → "Action Submitted" HTML page

Why not record the token on the initial GET?

If the token were recorded when the form is shown, any network error, browser back-navigation, or accidental page load would permanently consume the one-time-use slot and the user would see "Already Actioned" if they re-submit. By deferring recording to the POST, the form is safe to reload multiple times.

WhatsApp Button Path

WhatsApp interactive buttons carry a payload string (max 256 chars). DirectAction uses the BUTTONPAYLOADPREFIX pattern to encode the ContextId.

WhatsApp interactive message button payload format: "{ButtonPayloadPrefix}_{ContextId}" Example: "WF_APPROVE_5001" POST /MessageHubGenerator/RequestMessageHubGenerator │ (frameworkBaseUrl captured from HttpContext) │ └─ MessageHubWebhookBLL.MessageHubWebhook(dto, login, frameworkBaseUrl) │ ├─ IsButtonMessage? → extract payload "WF_APPROVE_5001" │ ├─ TryBuildTemplateApprovalDTO? (ACCEPT_/REJECT_/FORWARD_ prefix) │ → if matched: call TemplateApprovalBLL (legacy path) │ └─ TryRouteDirectActionAsync(payload, login, frameworkBaseUrl) │ ├─ GetAllActiveDetailsAsync → list all MDIRECTACTIONDETAIL rows ├─ FirstOrDefault where payload.StartsWith(prefix + "_") │ → matched: ButtonPayloadPrefix="WF_APPROVE" ├─ Parse ContextId: "5001" from "WF_APPROVE_5001"[10..] │ └─ ExecuteAsync("WORKFLOW_APPROVE", 5001, loginDTO.UserId, null, loginDTO, frameworkBaseUrl)
No token for WhatsApp buttons
WhatsApp interactive buttons do not use signed tokens — the user is already authenticated via WhatsApp. loginDTO.UserId is used as the assigneeUserId. One-time-use is not enforced for WhatsApp buttons (clicking multiple times will call the API multiple times; the API must be idempotent).

PayloadTemplate Substitution

The PAYLOADTEMPLATE column in MDIRECTACTIONDETAIL is a JSON string with two supported placeholders:

PlaceholderReplaced withSource
{ContextId}Numeric entity PKFrom token or WhatsApp button payload
{Remarks}User-provided text (or empty string)From NeedsInput=1 form POST; empty if NeedsInput=0

Examples

-- NeedsInput=0: Approve without comments
PAYLOADTEMPLATE = '{"Items":[{"TaskId":{ContextId},"Action":1}]}'
After substitution (ContextId=5001):
  {"Items":[{"TaskId":5001,"Action":1}]}

-- NeedsInput=1: Reject with reason
PAYLOADTEMPLATE = '{"Items":[{"TaskId":{ContextId},"Action":2,"Remarks":"{Remarks}"}]}'
After substitution (ContextId=5001, Remarks="Budget not available"):
  {"Items":[{"TaskId":5001,"Action":2,"Remarks":"Budget not available"}]}
Remarks are plain-text substituted — not JSON-escaped
If Remarks may contain quotes or backslashes, ensure the API endpoint handles these gracefully. Consider configuring NeedsInput=1 actions so Remarks are placed in string values only, not as object keys.

Resend Flow

POST /Action/Resend
Body: { "EventTypeId": 42, "ContextId": 5001 }

→ Publishes new OutboxEvent to TOUTBOX
→ Action pipeline re-executes → new TEVENTACTIONRUN
→ EmailActionHandler generates FRESH tokens (48h from resend time)
→ New email sent with new buttons

Old tokens remain valid until their original 48-hour expiry. The one-time-use check in TDIRECTACTIONTOKEN prevents double-execution — whichever token is clicked first wins. Old tokens can be manually revoked by setting STATUS=2 in TDIRECTACTIONTOKEN.

EIP Database Tables

MEIPFLOWDEFINITION

CREATE TABLE MEIPFLOWDEFINITION (
    FLOWDEFINITIONID  int           NOT NULL PRIMARY KEY,
    TENANTID          int           NOT NULL,
    FLOWCODE          nvarchar(100) NOT NULL,
    FLOWVERSION       int,
    FLOWSTATUS        int,           -- 1=active, 0=archived
    JSONDEFINITION    nvarchar(max), -- full flow JSON (StartStepCode + Steps dict)
    VERSION           int,
    STATUS            int,           -- 1=active (soft delete)
    CREATEDBYID       int,
    CREATEDON         datetime2,
    MODIFIEDBYID      int,
    MODIFIEDON        datetime2,
    CONSTRAINT UX_FLOWCODE UNIQUE (FLOWCODE, TENANTID, STATUS)
);

MEIPROUTINGRULE

CREATE TABLE MEIPROUTINGRULE (
    ROUTINGRULEID     int           NOT NULL PRIMARY KEY,
    TENANTID          int           NOT NULL,
    FLOWDEFINITIONID  int           NOT NULL, -- FK → MEIPFLOWDEFINITION
    ENDPOINTID        int           NOT NULL, -- FK → MEIPCHANNELENDPOINT
    MATCHTYPE         int,          -- 1=EQUALS, 2=STARTS_WITH, 3=CONTAINS, 4=REGEX
    MATCHVALUE        nvarchar(500),
    PRIORITY          int,          -- lower = higher priority
    STATUS            int,          -- 1=active
    VERSION           int,
    CREATEDBYID       int,
    CREATEDUTC        datetime2,
    MODIFIEDBYID      int,
    MODIFIEDUTC       datetime2
);

TEIUSERSESSION

CREATE TABLE TEIUSERSESSION (
    SESSIONID         uniqueidentifier NOT NULL PRIMARY KEY,
    USERIDENTIFIER    nvarchar(255),  -- phone/email/userId
    TENANTID          int,
    CHANNELTYPE       int,            -- EIPChannelType enum value
    FLOWCODE          nvarchar(100),
    CURRENTSTEPCODE   nvarchar(100),
    CONTEXTJSON       nvarchar(max),  -- JSON dict of Variables
    STARTEDAT         datetime2,
    LASTACTIVITYAT    datetime2,      -- 30-min TTL check
    STATUS            int             -- 1=active, 0=expired
);
CREATE INDEX IX_TEIUSERSESSION_LOOKUP
    ON TEIUSERSESSION (USERIDENTIFIER, TENANTID, CHANNELTYPE) WHERE STATUS = 1;

DirectAction Database Tables

MDIRECTACTION (Header)

CREATE TABLE MDIRECTACTION (
    DIRECTACTIONID      int           NOT NULL PRIMARY KEY,
    DIRECTACTIONCODE    nvarchar(20),  -- "WFAPPROVAL", "PO_APPROVAL"
    DIRECTACTIONNAME    nvarchar(200), -- display name
    CONTEXTIDFIELD      nvarchar(200), -- field name in entity payload for the PK
    ASSIGNEEUSERIDFIELD nvarchar(200), -- field name for assignee user ID
    TENANTID            int,           -- -1 = global template
    STATUS              int,
    VERSION             int,
    SORTORDER           int,
    SOURCETYPE          int,
    CREATEDBYID         int,
    CREATEDON           datetime2,
    MODIFIEDBYID        int,
    MODIFIEDON          datetime2
);

MDIRECTACTIONDETAIL (Buttons)

CREATE TABLE MDIRECTACTIONDETAIL (
    DIRECTACTIONDETAILID  int            NOT NULL PRIMARY KEY,
    DIRECTACTIONID        int            NOT NULL,  -- FK → MDIRECTACTION
    SLNO                  smallint,                  -- display order
    ACTIONCODE            nvarchar(20),  -- "WORKFLOW_APPROVE"
    ACTIONLABEL           nvarchar(200), -- "Approve" (shown in form title for NeedsInput=1)
    EMAILPLACEHOLDER      nvarchar(100), -- "APPROVE_URL" → ##APPROVE_URL## in email template
    BUTTONPAYLOADPREFIX   nvarchar(100), -- "WF_APPROVE" → WhatsApp button payload prefix
    APIENDPOINT           nvarchar(500), -- "/WorkFlow/WorkFlowActions"
    HTTPMETHOD            nvarchar(10),  -- "POST", "PUT", "PATCH", "DELETE"
    PAYLOADTEMPLATE       nvarchar(max), -- JSON with {ContextId} and {Remarks}
    NEEDSINPUT            tinyint,       -- 0=direct, 1=show remarks form
    FLOWID                int            -- FK → MEIPFLOWDEFINITION (-1 if N/A)
);

TDIRECTACTIONTOKEN (One-time-use audit)

CREATE TABLE TDIRECTACTIONTOKEN (
    TOKENHASH       nvarchar(64)   NOT NULL PRIMARY KEY,  -- SHA256 hex of signed token
    CONTEXTID       int,
    ACTIONCODE      nvarchar(20),
    ASSIGNEEUSERID  int,
    TENANTID        int,
    EXPIRESAT       datetime,      -- original token expiry (48h from generation)
    USEDAT          datetime,      -- when clicked (NULL = never used / recorded on click)
    STATUS          tinyint,       -- 0=unused, 1=used, 2=revoked
    REMARKS         nvarchar(2000),-- user-provided remarks (NeedsInput=1 only)
    CREATEDON       datetime
);
CREATE INDEX IX_TDIRECTACTIONTOKEN_HASH_TENANT
    ON TDIRECTACTIONTOKEN (TOKENHASH, TENANTID);

Action Pipeline Tables

TEVENTACTIONRUN

-- Tracks execution lifecycle of each triggered action
ACTIONRUNID      int PK       -- app-generated sequence
ACTIONID         int          -- FK → MACTION
TENANTID         int
RUNSTATUS        int          -- 0=Pending, 1=InProgress, 2=Completed, 3=Failed
PAYLOAD          nvarchar(max)-- entity JSON at time of trigger
RESULT           nvarchar(max)-- execution outcome
ERRORMESSAGE     nvarchar(max)
ATTEMPTS         int
CORRELATIONKEY   nvarchar(255)-- for deduplication: ActionId + entity PK

TACTIONOUTBOX

-- Transactional outbox: reliable publish to Dapr/RabbitMQ
OUTBOXID          int PK
TENANTID          int
DESTINATIONTOPIC  nvarchar(100) -- "action-exec-p0" through "action-exec-p4" (tenantId % 5)
PAYLOAD           nvarchar(max) -- ActionEventDto JSON
SENDSTATUS        int           -- 0=Pending, 1=Sent, 2=Failed
ATTEMPTS          int
CREATEDON         datetime2

Edge Cases & Known Issues

ScenarioBehaviourNotes
Expired token click TryValidate returns false → "Link Expired or Invalid" HTML page Log contains: DirectActionToken: expired
Already-used token click (NeedsInput=0) TDIRECTACTIONTOKEN.UsedAt != null → "Already Actioned" page with timestamp
Already-used token — NeedsInput=1 form re-submitted ActionSubmitEndpoint finds existing record → "Already Actioned" page Safe: token recorded only once
Revoked token (STATUS=2) Found record with no UsedAt → "This link has been revoked." Manual revocation: UPDATE TDIRECTACTIONTOKEN SET STATUS=2 WHERE TOKENHASH=...
Two users click same pool-approval token simultaneously First INSERT wins; second finds existing record → "Already Actioned" Race condition window is very small (<1ms). A UNIQUE index on TOKENHASH further protects.
No routing rule matches incoming EIP message Phase 2 throws → error response returned to caller Add a catch-all CONTAINS rule with low priority
Flow exceeds 50 steps EIPFlowEngine throws InvalidOperationException("Maximum step count exceeded") Indicates a loop in the flow definition — check NextStepCode chains
EIP session expires (30 min idle) Next message starts a fresh session; routing resolves FlowCode again Variables from old session are lost — the conversation restarts
WhatsApp button clicked multiple times Each click calls ExecuteAsync — API is called multiple times API must be idempotent. No one-time-use enforcement for button path.
NeedsInput=1 token expired before user submits form ActionSubmitEndpoint: TryValidate fails → "Link Expired or Invalid" User must request a resend from the system

QC Test Scenarios

DirectAction — Email Path

#ScenarioInputExpected
DA-01Happy path NeedsInput=0Valid token, NeedsInput=0 action200 HTML "Action Successful"; TDIRECTACTIONTOKEN row inserted with UsedAt=now; internal API called once
DA-02Happy path NeedsInput=1Valid token, NeedsInput=1 actionGET → remarks form HTML; no TDIRECTACTIONTOKEN row yet; POST /Action/Submit → success HTML; row inserted with Remarks; API called once with Remarks in payload
DA-03Double click NeedsInput=0Same valid token clicked twiceFirst click: success. Second click: "Already Actioned" page with timestamp
DA-04Double submit NeedsInput=1Same form submitted twiceFirst submit: success. Second submit: "Already Actioned" page
DA-05Expired tokenToken with past expiresAt"Link Expired or Invalid" HTML page
DA-06Tampered tokenModify one character in payload section"Link Expired or Invalid" (signature mismatch)
DA-07Tampered contextIdDecode payload, change contextId=9999, re-encode without re-signing"Link Expired or Invalid" (signature mismatch)
DA-08Missing token paramGET /Action/Execute (no ?token=)"Invalid Link" HTML page
DA-09Revoked tokenSet TDIRECTACTIONTOKEN.STATUS=2, click link"This link has been revoked." page
DA-10API returns 400 business errorValid token, but target API returns Status=400Error HTML page with GeneralErrors message from API response
DA-11TDIRECTACTIONTOKEN.EXPIRESATClick valid 48h token immediatelyTDIRECTACTIONTOKEN.EXPIRESAT = generation_time + 48h (NOT click time)
DA-12ResendPOST /Action/Resend with EventTypeId + ContextIdNew email sent with fresh tokens; old tokens still work until their original expiry

DirectAction — WhatsApp Button Path

#ScenarioInputExpected
WA-01Happy path button pressPayload = "WF_APPROVE_5001", matching MDIRECTACTIONDETAIL.BUTTONPAYLOADPREFIXExecuteAsync called; API called; logged as success
WA-02Unknown button prefixPayload = "UNKNOWN_PREFIX_99"Warning logged: "did not match any DirectAction prefix"; no API call
WA-03Invalid ContextId in payloadPayload = "WF_APPROVE_abc"Warning logged: "Could not parse ContextId"; no API call
WA-04Double pressSame button pressed twiceAPI called twice (no one-time-use). API must be idempotent.

EIP Conversation Engine

#ScenarioInputExpected
EIP-01New conversationMessage matching a routing ruleSession created; flow starts at StartStepCode; response sent via channel
EIP-02Resume existing sessionSecond message from same user/channel within 30 minSession restored; flow resumes at CurrentStepCode
EIP-03INPUT validation failUser sends "abc" to a step with pattern ^\d{4}-\d{2}-\d{2}$Validation error message returned; step repeats
EIP-04CHOICE invalid selectionUser sends "4" when only 3 choices existStep repeats with "Invalid choice" message
EIP-05Session expirySecond message after 31+ min idleNew session; flow starts from beginning (old variables lost)
EIP-06Flow completes (END step)Conversation reaches END stepFinal message sent; session STATUS=0
EIP-07No matching routing ruleMessage with no matching MEIPROUTINGRULEError response; error logged
EIP-08Max steps exceededFlow definition has circular NextStepCodeInvalidOperationException after 50 steps; error response
EIP-09Rate limit>1 response to same user within 2 secondsSecond message dropped silently by ResponseEngine rate limiter
EIP-10ACTION step API failCALL_API endpoint returns 5xxActionEngine returns IsSuccess=false; flow error branch (or end if no error handling configured)
EIP-11QUALIFY happy pathStep type=QUALIFY; required input keys present in context; qualifier stage succeedsQualifierFacadeImpl.ExecuteStageAsync called; IsSuccess=true; Facts exported as JSON stored in OutputFactKey; flow advances to NextStepCode
EIP-12QUALIFY error findingQUALIFY step; qualifier returns Error findingFlow routes to OnValidationErrorStepCode; context.Variables["_QualifyError"] set to error message from finding
EIP-13DYNAMIC_CHOICE first visitSourceKey holds JSON array in context; first time step is reachedButtons built from array using LabelField/ValueField; response returned with button list; step remains paused awaiting selection
EIP-14DYNAMIC_CHOICE selectionUser sends a label or value matching one of the dynamic buttonsStoreSelectedKey populated with selected value; StoreItemKey populated with full item JSON; flow advances to NextStepCode
EIP-15LOCATION step resolutionInbound payload contains Location.Latitude; LOCATION step is currentPhase 1 sets _Location.Lat, _Location.Lng, _Location.Address in Variables; LOCATION step resolves immediately and advances without pausing again
EIP-16QrCodeData deliveryMESSAGE step with QrCodeData set; sent to AppChat, WhatsApp, and Telegram channelsAppChat receives QrCodeData string; WhatsApp handler sends image message type with qrserver.com link; Telegram handler calls sendPhoto API
EIP-17INPUT InputType=date validationUser sends "abc" to an INPUT step with Validation.InputType="date"Validation error re-prompt sent; step stays paused. User then sends valid date string → stored in ContextKey, flow advances
EIP-18INPUT InputType=rating validationUser sends "6" when RatingMax=5; then "3""6" → error re-prompt (exceeds RatingMax). "3" → stored in ContextKey, flow advances
EIP-19Voice channel end-to-endPOST /EIPConversation/Receive with ChannelType=7 (Voice)Response Status="VOICE_READY"; AdditionalData contains TtsText (plain text) and SsmlText (SSML markup from VoiceHint); no HTTP delivery call made

Key File Map

EIP Conversation Engine

ComponentPath (relative to GB5Framework/)
Main orchestratorFrameworkBLL/EIPConversation/EIPHandlers/EIPConversationEngine/EIPConversationEngine.cs
BLL wrapperFrameworkBLL/EIPConversation/EIPHandlers/EIPConversationBLL/EIPConversationBLL.cs
Phase 1 — NormalizerFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ChannelNormalizer/EIPChannelNormalizer.cs
Phase 2 — RoutingFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/RoutingEngine/EIPRoutingEngine.cs
Phase 3 — Flow EngineFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/FlowEngine/EIPFlowEngine.cs
Phase 4 — CapabilityFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/CapabilityEngine/EIPCapabilityEngine.cs
Phase 5 — Action EngineFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ActionEngine/EIPActionEngine.cs
Phase 5 — Action HandlersFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ActionHandler/*.cs
Phase 6 — Response EngineFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ResponseEngine/EIPResponseEngine.cs
Channel HandlersFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ChannelHandler/*.cs
Text FormatterFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/FlowEngine/EIPTextFormatter.cs
Voice HandlerFrameworkBLL/EIPConversation/EIPHandlers/EIPEngine/ChannelHandler/VoiceChannelHandler.cs
Session DALFrameworkDAL/CustomCode/EIPConversation/EIPSession/EIPSessionDAL.cs
Flow QB (SQL)FrameworkDAL/Query/EIPConversation/EIPFlow/EIPFlowQB.cs
Routing QB (SQL)FrameworkDAL/Query/EIPConversation/EIPRouting/EIPRoutingQB.cs
Receive EndpointFrameworkSL/Endpoints/EIPConversation/ReceiveEIPConversation.cs
Webhook Endpoint (WhatsApp)FrameworkSL/Endpoints/MessageHubGenerator/RequestMessageHubGenerator.cs

Extended EIP DTOs

DTOPath (relative to GB5Framework/)
EIPQualifyConfigDTOFrameworkDAL/DTO/EIPConversation/EIPQualifyConfigDTO.cs
EIPEnrichConfigDTOFrameworkDAL/DTO/EIPConversation/EIPEnrichConfigDTO.cs
EIPDynamicChoiceConfigDTOFrameworkDAL/DTO/EIPConversation/EIPDynamicChoiceConfigDTO.cs
EIPLocationDTOFrameworkDAL/DTO/EIPConversation/EIPLocationDTO.cs
EIPResponseFormatDTOFrameworkDAL/DTO/EIPConversation/EIPResponseFormatDTO.cs
EIPStructuredDataDTOFrameworkDAL/DTO/EIPConversation/EIPStructuredDataDTO.cs
EIPFlowExecutionResultDTO (extended)FrameworkDAL/DTO/EIPConversation/EIPFlowExecutionResultDTO.cs

DirectAction System

ComponentPath (relative to GB5Framework/)
Token Service (interface)FrameworkBLL/DirectAction/IDirectActionTokenService.cs
Token Service (impl)FrameworkBLL/DirectAction/DirectActionTokenService.cs
API Handler (interface)FrameworkBLL/DirectAction/IGenericApiDirectActionHandler.cs
API Handler (impl)FrameworkBLL/DirectAction/GenericApiDirectActionHandler.cs
Email Action HandlerFrameworkBLL/ActionProcessor/Handlers/EmailActionHandler.cs
Webhook BLLFrameworkBLL/MessageHubGenerator/MessageHubWebhook/MessageHubWebhookBLL.cs
Execute Endpoint (NI=0)FrameworkSL/Endpoints/Action/ActionExecuteEndpoint.cs
Submit Endpoint (NI=1)FrameworkSL/Endpoints/Action/ActionSubmitEndpoint.cs
Resend EndpointFrameworkSL/Endpoints/Action/ResendDirectAction.cs
Config DALFrameworkDAL/CustomCode/DirectAction/DirectActionConfigDAL.cs
Token DALFrameworkDAL/CustomCode/DirectAction/DirectActionTokenDAL.cs
Config QB (SQL)FrameworkDAL/Query/DirectAction/DirectActionConfigQB.cs
Token QB (SQL)FrameworkDAL/Query/DirectAction/DirectActionTokenQB.cs
DTOsFrameworkDAL/DTO/DirectAction/{DirectActionDTO, DirectActionDetailDTO, DirectActionTokenDTO}.cs
DB Migration — REMARKSDB/Migrations/TDIRECTACTIONTOKEN_Add_REMARKS.sql

GoodBooks GB5 — EIP & DirectAction Developer Reference — generated 2026-06-20