Automations & Approvals
Run event-driven workflows with triggers and actions, and gate sensitive changes behind approval locks.
For the worker process itself, see Background Services.
Automations
Two halves: the API module stores definitions and history, the automations-service executes them.
backend/core-api/src/modules/automations/
graphql/schema/{index,queries,mutations,types}.ts, typeDef.ts
graphql/resolvers/{queries,mutations,customResolver}
db/models/{Automations,Executions,AutomationEmailTemplates,
AutomationWorkflowTemplates}.ts
db/definitions/{automationEmailTemplate,automationWorkflowTemplate}.ts
trpc/{automations,clients,index}.ts
backend/core-api/src/meta/automations/ # core triggers/actions via startAutomations
backend/services/automations/src/
bullmq/{initMQWorkers,triggerWorker,actionHandlerWorker,aiWorker}.ts
executions/actions/{webhook,emailAction}/
mongo/waitingActionsToExecute.ts
ai/ # aiAgent, aiAction, provider bridge, knowledge
trpc/, constants.ts, main.ts # default port 3302
An Automation (types.ts) holds name, status, edgeType, flowDirection, tagIds, triggers: [Trigger], actions: [Action], workflows: [Workflow], duplicatedFrom, and resolves createdUser, updatedUser, and approvalLockState(action). Each run is an Execution record (db/models/Executions.ts) exposed through automationHistories.
GraphQL API
| Operation | Key arguments |
|---|---|
automationsMain / automations | page, perPage, ids, excludeIds, searchValue, sortField, sortDirection, status, tagIds, createdByIds, updatedByIds, date ranges, triggerTypes, actionTypes, cursor params |
automationDetail / cpAutomationDetail | _id! |
automationHistories / automationHistoriesTotalCount | automationId!, status, triggerId, triggerType, targetId, targetIds, parentExecutionId, failedActionIds, errorCodes, waitingActionIds, beginDate, endDate, cursor params |
getAutomationExecutionDetail | executionId! |
automationStats / automationExecutionCounts | automationId!, beginDate, endDate / automationIds: [String!]! |
automationConstants | none; returns the merged trigger/action registry |
automationSetPropertyTargets / automationNodeOutput / automationReferenceFields | sourceType! / nodeType! / type!, field! |
automationNotes | automationId!, triggerId, actionId |
automationsAiAgents / automationsAiAgentDetail / automationsAiAgentHealth | kind / _id / agentId! |
getAutomationWebhookEndpoint | _id!, waitEventActionId |
automationEmailTemplates / automationWorkflowTemplates | page, perPage, searchValue |
automationsAdd / automationsEdit | name, status, edgeType, flowDirection, triggers: [TriggerInput], actions: [ActionInput], workflows: [WorkflowInput] (automationsEdit takes _id, acknowledgeDuplicate) |
automationsDuplicate / automationsRemove / archiveAutomations | _id!, name / automationIds / automationIds, isRestore |
automationsSaveAsTemplate / automationsCreateFromTemplate | _id!, name, duplicate / _id |
automationsAddNote / automationsEditNote / automationsRemoveNote | automationId, triggerId, actionId, description |
automationsAiAgentAdd / Edit / Remove / Reindex | name, description, connection, runtime, context / _id!, fileId |
automationEmailTemplates* / automationWorkflowTemplates* | name!, description, content! / entryActionId, actions, inputs |
How plugins register triggers and actions
A plugin passes an AutomationConfigs object (meta/automations.ts → startPlugin); core calls startAutomations(app, 'core', config) from initAutomation in meta/automations/automations.ts. The config declares:
constants.triggers/constants.actions: the catalogautomationConstantsreturns. Sales mergessalesAutomationContantsandposAutomationConstants.receiveActions: executes the plugin's actions during a run.checkCustomTrigger: evaluates plugin trigger conditions.setProperties,findObject: backssetPropertyactions and target lookups (findObjectTargets,setPropertyTargets).generateAiContext,loadAiKnowledgeDocumentBatch,lookupAiTool: optional AI-agent hooks.
Handlers are typically composed with createCoreModuleProducerHandler, which dispatches by moduleName inside the plugin (sales routes sales vs pos).
A plugin trigger is missing from the builder
The plugin's meta/automations.ts constants must be registered and the plugin enabled. automationConstants shows what the gateway aggregated.
Execution path
initMQWorkers in the service starts three BullMQ workers (trigger, action, aiAgent) on the automations queue prefix. The API alone only stores definitions and history: with the service stopped, triggers queue but nothing executes. Waiting steps (delay actions, waitEvent webhook waits) persist in waitingActionsToExecute and resume via waitingActionIds history filters; getAutomationWebhookEndpoint exposes the inbound webhook URL. Built-in action types in constants.ts: delay, if, setProperty, sendEmail.
Approvals
backend/core-api/src/modules/approval/
graphql/schema/{index,types,queries,mutations}.ts
graphql/resolvers/{queries,mutations,customResolvers}.ts
db/definitions/{approvalLocks,approvalRequests}.ts
db/models/{ApprovalLocks,ApprovalRequests}.ts
trpc/approval.ts
backend/erxes-api-shared/src/core-modules/approval/
checkApprovalLock.ts, types.ts, zodSchemas.ts, constants.ts
An ApprovalLock records contentType, contentId, lockedBy, ownerIdSnapshot, allowedUserIds, approverScope, approvalMode, status, releasedAt/releasedBy/releaseReason. An ApprovalRequest tracks lockId, requesterId, reason, status, requiredApproverIds, per-user decisions[] (userId, decision, reason, at), notificationIds, resolvedAt.
| Operation | Key arguments |
|---|---|
approvalLockState | contentType!, contentId!, ownerId, action; returns locked, hasAccess, reason, lock, pendingRequest |
approvalLockStates | contentType!, contentIds: [String!]!, ownerIdsByContentId, action |
approvalRequests | status, contentType, requesterIds, approverIds, cursor params |
approvalRequestDetail | _id! |
approvalLockCreate | input: { contentType!, contentTypeId!, ownerId!, allowedUserIds, scope, mode } |
approvalLockRelease / approvalLockForceRelease | _id! / _id!, reason! |
approvalRequestCreate | input: { contentType!, contentId!, reason } |
approvalRequestApprove / Reject / Cancel | _id! / _id!, reason / _id! |
Plugins do not query the collections. They call checkApprovalLock.state({ subdomain, contentType, contentId, action, userId }) from erxes-api-shared, which reaches core over tRPC (module: 'approval', action: 'state'). If the check is unreachable it fails closed (locked: true, hasAccess: false, reason "Approval lock check unavailable"). The Document and Automation GraphQL types expose approvalLockState(action) so the UI can render lock state inline.
Locked for everyone
If approvalLockState reports locked for every user, the tRPC call to core failed: the lock check fails closed. Check core-api health before assuming a real lock exists.
Approvals are a separate feature, not an automation step type: automations pause on delay/waitEvent actions, while gated edits go through approvalLockCreate → approvalRequestCreate → approve/reject → approvalLockRelease.
Permissions
automationsRead (always), automationsCreate, automationsUpdate, automationsDelete, automationsAiAgentAdd, automationsAiAgentEdit, automationsAiAgentRemove; scope own (records in createdBy) or all. approvalLocksManage is always: true; approvalLocksForceRelease is restricted.
Example
query ExecutionHistory($automationId: String!) {
automationHistories(automationId: $automationId, status: "error", limit: 20) {
list { _id status createdAt }
totalCount
}
}
mutation RequestApproval {
approvalRequestCreate(input: {
contentType: "sales:deal"
contentId: "<dealId>"
reason: "Discount above threshold"
}) { _id status }
}