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 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.

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 }
}

Operate on one system.

Talk to our team about consolidating your stack onto erxes. 30-minute walkthrough, custom to your org.