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

OperationKey arguments
automationsMain / automationspage, perPage, ids, excludeIds, searchValue, sortField, sortDirection, status, tagIds, createdByIds, updatedByIds, date ranges, triggerTypes, actionTypes, cursor params
automationDetail / cpAutomationDetail_id!
automationHistories / automationHistoriesTotalCountautomationId!, status, triggerId, triggerType, targetId, targetIds, parentExecutionId, failedActionIds, errorCodes, waitingActionIds, beginDate, endDate, cursor params
getAutomationExecutionDetailexecutionId!
automationStats / automationExecutionCountsautomationId!, beginDate, endDate / automationIds: [String!]!
automationConstantsnone; returns the merged trigger/action registry
automationSetPropertyTargets / automationNodeOutput / automationReferenceFieldssourceType! / nodeType! / type!, field!
automationNotesautomationId!, triggerId, actionId
automationsAiAgents / automationsAiAgentDetail / automationsAiAgentHealthkind / _id / agentId!
getAutomationWebhookEndpoint_id!, waitEventActionId
automationEmailTemplates / automationWorkflowTemplatespage, perPage, searchValue
automationsAdd / automationsEditname, 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 / automationsRemoveNoteautomationId, triggerId, actionId, description
automationsAiAgentAdd / Edit / Remove / Reindexname, 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 catalog automationConstants returns. Sales merges salesAutomationContants and posAutomationConstants.
  • receiveActions: executes the plugin's actions during a run.
  • checkCustomTrigger: evaluates plugin trigger conditions.
  • setProperties, findObject: backs setProperty actions 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.

OperationKey arguments
approvalLockStatecontentType!, contentId!, ownerId, action; returns locked, hasAccess, reason, lock, pendingRequest
approvalLockStatescontentType!, contentIds: [String!]!, ownerIdsByContentId, action
approvalRequestsstatus, contentType, requesterIds, approverIds, cursor params
approvalRequestDetail_id!
approvalLockCreateinput: { contentType!, contentTypeId!, ownerId!, allowedUserIds, scope, mode }
approvalLockRelease / approvalLockForceRelease_id! / _id!, reason!
approvalRequestCreateinput: { 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 }
}
Was this helpful?