Subprocess y Call Activity¶
Dos formas de componer procesos: embedded subprocess (mismo proceso, scope nested) y call activity (proceso separado, independiente). Diferencias en lifecycle, variables, ejecución, y trade-offs prácticos.
Comparación a 30 segundos¶
| Aspecto | Embedded Subprocess | Call Activity |
|---|---|---|
| Compilación | Dentro del proceso padre | Proceso independiente |
| Versionado | Junto con el padre | Versionado propio (puede cambiar sin redeploy parent) |
| Variables | Comparten scope con padre | Aisladas; mapping explícito |
| Lifecycle | Termina con el padre | Puede sobrevivir al padre (rare) |
| Reutilización | NO entre procesos | SÍ entre procesos |
| Granularidad | Bloque lógico interno | Servicio compartido |
| Ejecución | Misma instance | Crea nueva instance |
| Operate UI | Drill-down dentro del padre | Link a otra instance |
Embedded Subprocess¶
Modelo BPMN¶
<bpmn:subProcess id="paymentBlock" name="Payment block">
<bpmn:startEvent id="paymentStart"/>
<bpmn:serviceTask id="charge" name="Charge">
<bpmn:extensionElements>
<zeebe:taskDefinition type="charge-payment"/>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:endEvent id="paymentEnd"/>
<!-- flows entre start, charge, end -->
</bpmn:subProcess>
Lifecycle¶
flowchart LR
A[Subprocess ACTIVATE] --> B[Ejecuta start event]
B --> C[Tokens fluyen]
C --> D[Todos los end events alcanzados]
D --> E[COMPLETE]
Variables — scope rules¶
flowchart TD
subgraph Root[Process variables - root scope]
V1[orderId, totalAmount]
end
subgraph Sub[Subprocess local scope]
V2[chargedAt<br/>local, invisible fuera]
end
Root -->|visibles dentro del subprocess| Sub
Sub -.->|al COMPLETE del subproc, las variables<br/>locales pueden propagarse vía outputMapping| Root
- Lectura: subprocess ve variables del padre (transparente).
- Escritura: si haces
set variablesin scope explícito, escribe en el scope local del subprocess. - Propagación a padre: variable se escribe en el padre si:
- El subprocess hace
set variable local=false(raro), o - El subprocess tiene un
outputMappingque las mapea al padre. - Al terminar: variables locales se descartan; sólo las mapeadas al padre persisten.
Cuándo usar embedded¶
✓ Agrupar lógica relacionada para legibilidad. ✓ Aplicar boundary events al grupo (timer, error) en lugar de a una task. ✓ Compensation scope. ✓ Multi-instance que envuelve varias tasks.
✗ Lógica reutilizable en múltiples procesos → usar Call Activity.
Casos típicos¶
1. Boundary timer sobre múltiples tasks
flowchart LR
subgraph SP["Subprocess 'approval-block'"]
MR[Manager review] --> DR[Director review]
end
SP -.->|timer 24h| E[Escalate]
Sin subprocess, tendrías que aplicar el timer a cada task individualmente.
2. Compensation scope
flowchart TD
subgraph SP["Subprocess 'booking'"]
BF["Book flight (con comp)"]
BH["Book hotel (con comp)"]
P[Pay]
P -.->|error| TC[Throw compensate]
TC --> LIFO[Comps LIFO solo del subprocess]
end
3. Event subprocess
flowchart TD
subgraph MF["Subprocess 'main flow'"]
LN[... lógica normal ...]
end
subgraph ES["Event subprocess<br/>(start = message 'cancel-order')"]
NC[Notify customer] --> E[End]
end
Cancelable desde un mensaje externo sin diseñar paths explícitos.
Call Activity¶
Modelo BPMN¶
<bpmn:callActivity id="processOrder" name="Process Order">
<bpmn:extensionElements>
<zeebe:calledElement processId="order-fulfillment"
propagateAllChildVariables="false"/>
<zeebe:ioMapping>
<zeebe:input source="=orderId" target="orderId"/>
<zeebe:input source="=customer.email" target="customerEmail"/>
<zeebe:output source="=trackingNumber" target="trackingNumber"/>
</zeebe:ioMapping>
</bpmn:extensionElements>
</bpmn:callActivity>
Lifecycle¶
flowchart LR
A[Call activity ACTIVATE] --> B[Engine crea<br/>nueva instance del<br/>proceso target]
B --> C[Padre queda WAITING]
C --> D[Child completa]
D --> E[Variables mapped back]
E --> F[Padre continúa]
Detalle:
1. El padre crea una nueva process_instance con parent_instance_key=<calling instance>.
2. La nueva instance ejecuta independientemente (mismo proceso del engine, otra entrada en la tabla).
3. El padre queda con un token en estado WAITING.
4. Cuando la child completa, dispara un evento que el padre captura.
5. El outputMapping copia variables del child al padre.
Variables — mapping explícito¶
flowchart TD
P["Parent process variables<br/>orderId, customer={email, name}"]
P -->|"inputMapping (explícito)<br/>orderId → orderId<br/>customer.email → customerEmail"| C1["Child process starts with:<br/>{orderId, customerEmail}"]
C1 -->|child executes, accumulates| C2["{orderId, customerEmail,<br/>trackingNumber, ...}"]
C2 -->|"outputMapping al COMPLETE<br/>trackingNumber → trackingNumber"| PR["Parent receives: trackingNumber<br/>(parent vars updated)"]
propagateAllChildVariables="false" (recomendado): solo las variables del outputMapping vuelven al padre. true (NO recomendado): TODAS las variables del child sobreescriben las del padre — fuente de bugs.
Cuándo usar Call Activity¶
✓ Reutilizar un proceso desde múltiples padres (e.g., "validate customer" desde "create order" y "update subscription"). ✓ Versionar independientemente la sub-lógica. ✓ Aislar variables para evitar conflictos de nombre. ✓ Separar ownership entre equipos (cada equipo dueño de su proceso). ✓ Multi-tenant: tenant-shared subproceso, instances aisladas.
✗ Bloque lógico de un solo uso → embedded. ✗ Loop muy pequeño donde el overhead de crear nueva instance pesa.
Resolución de versión del child¶
Binding types:
| Type | Resolution | Use case |
|---|---|---|
latest (default) |
Última versión deployed | Dev / pre-prod |
deployment |
Misma versión deployment | Coherencia entre procesos relacionados |
versionTag |
Tag específico (v1.2.3) |
Producción con control de cambios |
Nuestra implementación M1: solo latest y versionTag. deployment agrega complejidad por las relaciones implicit.
Persistencia child-parent¶
-- Tabla process_instances
parent_process_instance_key BIGINT REFERENCES process_instances(key),
parent_element_instance_key BIGINT, -- el call activity en el padre
CREATE INDEX ON process_instances (parent_process_instance_key);
Permite: - "Cancel parent" cascada a child (opcional). - Operate: drill-down de parent a child y back. - Métricas: instance count incluye o excluye sub-instances.
Cancelación¶
wf instance cancel <parent-key>→ cancela también las child instances activas (cascada).wf instance cancel <child-key>→ child cancelled; parent recibe un evento de "subprocess failed" que puede ser handled (boundary error event).
Diferencias semánticas críticas¶
Error handling¶
Embedded: error inside subproc → busca boundary handler en subproc → si no hay, sube al padre.
Call Activity: error inside child → si child no lo maneja, child queda en incident. El padre no ve el error automáticamente. Para reaccionar, el padre necesita un boundary error event en el call activity.
<bpmn:callActivity id="processOrder">...</bpmn:callActivity>
<bpmn:boundaryEvent attachedToRef="processOrder">
<bpmn:errorEventDefinition errorRef="ChildFailed"/>
</bpmn:boundaryEvent>
El child debe propagar el error explícitamente vía <bpmn:endEvent> con error end definition.
Compensation¶
Embedded: throw compensation dentro del subproc compensa elementos del subproc. Throw fuera puede o no incluir el subproc según activityRef.
Call Activity: compensation NO cruza el boundary. Cada proceso compensa lo suyo. Si necesitás cross-process compensation, el padre debe orquestar explícitamente.
Timer / Boundary events¶
Embedded: timer boundary sobre el subproc afecta a TODO el contenido. Interrupting timer cancela todos los tokens dentro.
Call Activity: timer boundary sobre call activity cancela el child completo (cascading cancel).
Performance considerations¶
Embedded subprocess¶
- Overhead: 1 element instance extra (el subproc en sí).
- Memory: variables comparten scope, sin duplicación.
- Replay: misma instance, mismo command log.
Call Activity¶
- Overhead: nueva process_instance (insert en tabla, indexes).
- Memory: variables duplicadas (parent + child).
- Replay: dos command logs ligados.
- Latencia: ~1-5ms extra para crear child + correlate al parent.
Para procesos high-throughput, preferir embedded a menos que la reutilización justifique.
Multi-tenancy¶
Embedded: tenant del padre.
Call Activity: child hereda tenant del padre por defecto. Permitir cross-tenant call activity es ANTI-PATTERN; bloqueado por defecto.
flowchart LR
A[Tenant A process] --> B[Call activity]
B -->|debe llamar| C[Proceso de tenant A]
B -.->|si intenta llamar proceso tenant B| D[BPMN_TENANT_MISMATCH error]
Recursión¶
Es legal: un proceso puede call activity a sí mismo (recursión).
flowchart TD
P["Process 'tree-traversal'"] --> FC[For each child]
FC --> CA["Call activity 'tree-traversal'<br/>(recursive)"]
CA -.->|recursión| P
Riesgos: - Stack overflow lógico (process instances anidadas). - Sin límite, una recursión runaway puede crear miles de instances.
Mitigación:
- Configurable max_call_activity_depth: 10 (default).
- Métrica de profundidad: wf_engine_call_activity_depth{process_id} histogram.
- Alerta si depth > 80% del max.
Testing patterns¶
// Test: child completes, parent continues
func TestCallActivityHappyPath(t *testing.T) {
parent := DeployProcess("parent.bpmn")
child := DeployProcess("child.bpmn")
instance := StartInstance(parent, map[string]any{"orderId": "O-1"})
WaitForCallActivityActive(instance, "processOrder")
childInstance := GetChildInstance(instance, "processOrder")
CompleteJob(childInstance, "do-work", map[string]any{"result": "ok"})
WaitForCompletion(instance)
AssertVariable(instance, "result", "ok")
}
// Test: child fails, parent handles via boundary
func TestCallActivityErrorBoundary(t *testing.T) {
// child raises BPMN error "validation-failed"
// parent has boundary error event with code="validation-failed"
// assert: parent activates error handler path
}
Operate UI considerations¶
- Embedded: drill-down dentro de la misma instance. Breadcrumb:
Process > Subprocess "X" > Task "Y". - Call Activity: link a otra instance. Breadcrumb cruza la frontera.
flowchart TD
P[Parent instance #1234] --> E["Element 'processOrder'<br/>(CallActivity)"]
E -->|link| C[view child instance #5678]
Decisión: cuándo cuál¶
Usar embedded subprocess si: - Boundary event aplicable a un grupo. - Compensation scope. - Event subprocess. - Lógica de un solo uso.
Usar call activity si: - Reutilización entre procesos. - Versionado independiente. - Equipos diferentes lo mantienen. - Aislamiento de variables fuerte.
Usar service task simple si: - Es UNA action. - No requiere modelado interno.
Referencias¶
- concepts/bpmn-execution-model — semántica general
- concepts/compensation-and-bpmn-errors — comp en subprocess vs call
- concepts/variable-scoping — scope rules
- analysis/bpmn-coverage-matrix — fase de soporte