Production engineering guide for erp workflow automation covering patterns, implementation strategies, and operational best practices.
ERP workflow automation orchestrates business processes across departments using real-time, event-driven triggers and structured task routing. It matters when a purchase requisition must initiate vendor onboarding, inventory allocation, and financial posting—each step synchronized with approval gates, notifications, and data consistency checks. The workflow is not a sequence of manual steps but a repeatable, monitored, and executable pipeline, where each process instance is a distinct entity with its own state, history, and metadata.
A workflow is a declarative specification of how a business process unfolds. It consists of a sequence of steps, each with a defined action, input, output, and execution condition. The core unit is the *workflow definition*, a JSON document describing the process structure, including:
id: unique identifier for the workflow (e.g., purchase-requisition-approval)version: semantic version (e.g., 1.2.0)start: the initial step, with type: "event-trigger" and event: "purchase.requisition.created"steps: an array of step definitions, each with:id: unique within the workflowtype: one of task, service-call, condition, timer, parallelinputs: map of variables to be passed to the stepoutputs: map of data produced by the stepconditions: optional if expression evaluating to true or falsenext: reference to the next step or null for final step{
"id": "purchase-requisition-approval",
"version": "1.2.0",
"start": {
"type": "event-trigger",
"event": "purchase.requisition.created"
},
"steps": [
{
"id": "validate-requisition",
"type": "service-call",
"service": "inventory.validate",
"inputs": {
"itemCode": "{{requisition.itemCode}}",
"quantity": "{{requisition.quantity}}"
},
"outputs": {
"available": "{{result.available}}",
"leadTime": "{{result.leadTime}}"
},
"next": "assign-approver"
},
{
"id": "assign-approver",
"type": "task",
"title": "Approve Purchase Requisition",
"assignee": "{{requisition.requestedBy}}",
"dueInDays": 3,
"next": "notify-approver"
},
{
"id": "notify-approver",
"type": "service-call",
"service": "notification.send",
"inputs": {
"to": "{{approver.email}}",
"subject": "Approve Requisition #{{requisition.id}}",
"body": "The requisition for {{requisition.quantity}} units of {{requisition.itemCode}} is pending approval."
},
"next": "wait-for-approval"
},
{
"id": "wait-for-approval",
"type": "condition",
"expression": "{{approval.status}} == 'approved'",
"trueNext": "create-purchase-order",
"falseNext": "escalate-to-manager"
}
]
}
Each step may produce side effects: creating a record, sending an email, triggering a webhook, or updating a database. The workflow engine tracks the state of each instance, stored in a workflow_instances table with fields: id, workflow_id, state, data, started_at, completed_at, error.
When an event occurs—such as purchase.requisition.created—the workflow engine looks up the purchase-requisition-approval definition and creates a new workflow_instance with state: "pending". The engine then executes the start step, which may be a service call, a timer, or a message queue trigger.
The event is a structured payload, typically JSON sent via HTTP or message queue (e.g., RabbitMQ, Kafka):
{
"event": "purchase.requisition.created",
"timestamp": "2024-04-05T12:34:56Z",
"data": {
"id": "PR-2024-001",
"requestedBy": "[email protected]",
"itemCode": "LAP-1000",
"quantity": 15,
"department": "IT",
"approved": false
}
}
The engine deserializes the event, populates the workflow instance’s data field, and begins execution. Each step runs asynchronously. The engine maintains a pending_steps queue and a workflow_instances table.
If a step fails (e.g., inventory.validate returns a 503), the instance enters state: "failed" and logs the error in workflow_instance.errors:
{
"stepId": "validate-requisition",
"error": {
"code": "service-unavailable",
"message": "Inventory service returned 503: Service Unavailable",
"details": {
"httpStatus": 503,
"responseBody": "{\"error\":\"Service is overloaded\",\"retryAfter\":60}"
}
}
}
The workflow can retry failed steps with configurable policies: maxRetries, delay, backoff: exponential, linear, or fixed.
Conditions are evaluated using a simple expression language. Expressions can reference any variable from the workflow instance or step outputs. The engine supports logical, comparison, and function expressions.
"expression": "{{approval.status}} == 'approved' and {{requisition.quantity}} > 10"
Conditions may be if, switch, or case blocks. The next field can be a single step ID or a list of steps for parallel branching.
{
"id": "approve-shipment",
"type": "condition",
"expression": "{{shipment.totalValue}} > 10000",
"trueNext": ["send-to-approval", "notify-finance"],
"falseNext": "record-inventory"
}
The engine evaluates the condition and schedules all trueNext steps to run in parallel. Each step runs independently, and the parent workflow waits for all to complete before proceeding.
A workflow step often calls an external service via HTTP. The service-call step uses a service registry to resolve endpoints, authentication, and input/output mapping.
The service registry is a table with:
name: unique identifier (e.g., inventory.validate, notification.send)endpoint: base URL (e.g., https://inventory.company.com/api/v1)auth: authentication method (basic, bearer, oauth2)inputMap: JSON mapping from workflow variables to request bodyoutputMap: mapping from response to workflow variablesExample service entry:
{
"name": "inventory.validate",
"endpoint": "https://inventory.company.com/api/v1/validate",
"auth": {
"type": "bearer",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
},
"inputMap": {
"itemCode": "itemCode",
"quantity": "quantity"
},
"outputMap": {
"available": "result.available",
"leadTime": "result.leadTime"
}
}
The engine sends a POST request to https://inventory.company.com/api/v1/validate with:
{
"itemCode": "LAP-1000",
"quantity": 15
}
It expects a 200 OK response with Content-Type: application/json and maps the result to variables as defined.
If the service returns 400 Bad Request, the engine captures the error and logs it. If the service is unreachable, the step retries after a delay.
Each workflow instance maintains a state field that tracks progress. Possible states include:
pending: initial state, awaiting event or first steprunning: a step is in progressfailed: a step failed and is being retriedcompleted: all steps finished successfullycancelled: explicitly stoppedtimeout: a timer step expiredThe engine uses a state table:
CREATE TABLE workflow_instances (
id UUID PRIMARY KEY,
workflow_id TEXT NOT NULL,
state TEXT NOT NULL,
data JSONB,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
error JSONB,
version TEXT
);
The data field is a JSONB column that stores the state of the workflow instance. It may include:
requisition: the original event dataapproval: the approval step's current stateinventory: the result of the validate-requisition steptimestamp: when a step was executedWhen a step fails, the engine updates the instance and logs the error. It may also emit a workflow.failed event:
{
"event": "workflow.failed",
"timestamp": "2024-04-05T12:40:00Z",
"data": {
"instanceId": "wfi-2024-001",
"workflowId": "purchase-requisition-approval",
"stepId": "assign-approver",
"error": {
"code": "assignee-not-found",
"message": "User [email protected] not in system"
}
}
}
A failed instance can be manually resumed, retried, or escalated. The engine supports *compensation steps*: actions executed when a workflow fails, such as sending a Slack alert or rolling back a purchase order.
A workflow engine exposes an HTTP API for monitoring and control. Key endpoints:
GET /workflows/{id}/instances — list all instances of a workflowGET /workflows/instances/{instanceId} — get full details of one instancePOST /workflows/instances/{instanceId}/retry — restart a failed stepPOST /workflows/instances/{instanceId}/cancel — stop an instanceGET /workflows/instances/{instanceId}/events — list all events and stepsExample request to resume a failed instance:
POST /workflows/instances/wfi-2024-001/retry HTTP/1.1
Host: workflow-engine.company.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"stepId": "assign-approver",
"reason": "User was on leave; reassigning to manager"
}
The engine responds with 200 OK and updated instance state.
The engine logs every step execution to a step_logs table:
CREATE TABLE step_logs (
id UUID PRIMARY KEY,
instance_id UUID REFERENCES workflow_instances(id),
step_id TEXT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
status TEXT, -- "success", "failed", "running"
inputs JSONB,
outputs JSONB,
error JSONB
);
A common failure mode is missing or misnamed variables. The engine raises a variable-not-found error when a step references a variable that does not exist in the instance data.
Another sharp edge: timing out on long-running workflows. A timer step can be configured with duration: "PT24H" (24 hours), but if the engine restarts, it may lose the timer. The engine must persist pending timers to a timers table and use a background job to check for expired timers every minute.
event-unknown and queues the event for later processing.itemCode, but the event sends itemCode with capitalization ItemCode. The engine uses camelCase to snake_case mapping, but this is not always configured.version: "1.0.0" while the definition has version: "1.1.0". The engine uses version: "1.0.0" and may miss new fields.event_id as a deduplication key and ignores duplicate events.A typical workflow runs end-to-end:
purchase.requisition.created with id: "PR-2024-001".purchase-requisition-approval and creates workflow_instance with state: "pending".validate-requisition step: calls inventory.validate service.available: true, leadTime: 5.assign-approver, setting assignee: "[email protected]", dueInDays: 3.notify-approver step.wait-for-approval.approval.status: "approved".approval.status == 'approved' and requisition.quantity > 10 → true.create-purchase-order and notify-finance.completed state.The engine records the full history, including all step logs, event payloads, and error details. A dashboard can now show the average time from requisition to purchase order, the number of failed approvals, and the most common errors.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy