Connector Sdk Architecture
El Connector SDK de Camunda 8 tiene dos primitivas:
OutboundConnectorFunction(singleexecute()method, stateless) yInboundConnectorExecutable(lifecycleactivate/deactivate, long-running, async). Outbound corre como job worker. Inbound corre continuamente y correlaciona eventos a procesos. El runtime maneja secret resolution + validation + deserialization encontext.bindVariables(Class<T>)— una sola llamada.
Las dos primitivas¶
OutboundConnectorFunction¶
public interface OutboundConnectorFunction {
Object execute(OutboundConnectorContext context) throws Exception;
}
Características:
- Stateless: cada invocación es independiente
- Single method: solo execute(), sin lifecycle
- Synchronous: el runtime espera el retorno
- Excepciones permitidas: cualquier checked exception se captura por el runtime
- Return type flexible: any serializable object o un ConnectorResponse subtype
Modelo de ejecución: el connector corre como job worker. El runtime polls/streams jobs de Zeebe, llama execute(), completa el job con el resultado.
InboundConnectorExecutable¶
public interface InboundConnectorExecutable<T extends InboundConnectorContext> {
void activate(T context) throws Exception;
void deactivate() throws Exception;
}
Características:
- Stateful: mantiene estado entre eventos (sockets, suscripciones, etc.)
- Lifecycle: activate al deploy del proceso, deactivate al undeploy
- Async required: activate() no puede bloquear el calling thread
- Long-running: corre indefinidamente hasta deactivate()
Modelo de ejecución: el runtime detecta procesos deployados con inbound connectors. Por cada uno, llama activate(). El connector se subscribe a su fuente de eventos (HTTP server, Kafka topic, polling timer). Al recibir un evento, lo correlaciona vía context.correlate().
El "magic" de bindVariables¶
public interface OutboundConnectorContext extends DocumentFactory {
JobContext getJobContext();
<T> T bindVariables(Class<T> cls);
}
Una sola llamada hace tres cosas:
- Deserialize: variables del job (JSON/MessagePack) a un POJO Java
- Replace secrets:
{{secrets.MY_TOKEN}}se reemplaza por el valor real - Validate: Bean Validation (
@NotEmpty,@Min, etc.) se aplica
Resultado: el desarrollador del connector solo declara un record/clase:
public record HttpRequest(
@NotEmpty String url,
@NotEmpty String method,
Map<String, String> headers,
@Valid Body body
) {}
// En el execute():
HttpRequest req = context.bindVariables(HttpRequest.class);
// req.url ya tiene el valor real, secretos resueltos, validado
Secret resolution¶
Chain of responsibility con múltiples providers:
flowchart TD
Input["{{secrets.SLACK_TOKEN}}"] --> P1[EnvironmentVariableSecretProvider<br/>SLACK_TOKEN env var]
P1 -->|null| P2[HashiCorpVaultSecretProvider<br/>secret/data/slack#token]
P2 -->|null| P3[AWSSecretsManagerSecretProvider<br/>arn:aws:...]
P3 -->|null| P4[AzureKeyVaultSecretProvider]
P4 -->|null| P5[GoogleSecretManagerSecretProvider]
P1 -.->|no-null| Result[Primer provider<br/>que retorna no-null gana]
P2 -.->|no-null| Result
P3 -.->|no-null| Result
P4 -.->|no-null| Result
P5 -.->|no-null| Result
Configuración via runtime properties o env vars. El connector ignora completamente qué provider se usa.
Inbound correlation¶
InboundConnectorContext.correlate(CorrelationRequest) maneja:
flowchart TD
Event[External event arrives] --> IC[InboundConnector]
IC --> CanAct{canActivate<br/>variables?}
CanAct -->|false| Ignore1[Ignore o forward error]
CanAct -->|true| Correlate[correlate request]
Correlate --> Match[Buscar process definition matching<br/>message name, start event subscription, etc.]
Match --> Type{Tipo de evento?}
Type -->|start event| Create[Crear process instance nueva]
Type -->|intermediate event| Existing[Correlacionar a instance existente]
Create --> Result[CorrelationResult]
Existing --> Result
Result -->|success| Wait[Seguir esperando próximo evento]
Result -->|failure| Strategy{Failure strategy}
Strategy --> Forward[ForwardErrorToUpstream<br/>responder error al sistema externo]
Strategy --> Drop[Ignore — silent drop]
CorrelationFailureHandlingStrategy¶
| Estrategia | Caso de uso |
|---|---|
ForwardErrorToUpstream |
Webhook responde 500, upstream reintentará |
Ignore |
Mensaje Kafka que no aplica, no es error |
El connector decide la estrategia según el tipo de evento.
Discovery de connectors¶
El runtime detecta connectors via:
Classpath scanning + annotations¶
@OutboundConnector(
name = "MyConnector",
inputVariables = {"url", "method"},
type = "io.camunda:my-connector:1"
)
public class MyConnectorFunction implements OutboundConnectorFunction { ... }
@InboundConnector(
name = "Webhook",
type = "io.camunda:webhook:1"
)
public class WebhookConnector implements InboundConnectorExecutable<...> { ... }
Al startup, el runtime scanea el classpath, encuentra clases con las anotaciones, las registra contra el type declarado.
Element Template Generator¶
Generador que produce JSON files para Camunda Web Modeler:
- Lee las anotaciones del POJO de input del connector
- Genera estructura JSON con fields, validations, defaults
- Output:
template.jsonupload-able a Web Modeler - Web Modeler muestra UI customizada para ese connector
Esto permite que el connector controle su UI declarativamente en código:
public record HttpRequest(
@Property(label = "URL", description = "Target URL")
@NotEmpty
String url,
@Property(label = "Method", choices = {"GET", "POST", "PUT", "DELETE"})
@NotEmpty
String method
) {}
→ Web Modeler muestra dropdown para method, text input para URL.
Runtime variants¶
| Variant | Use case |
|---|---|
connector-runtime-application |
Standalone JAR, configura via env vars/YAML, conecta a Zeebe via gRPC |
connector-runtime-spring |
Embedded en Spring Boot app, beans son connectors |
spring-boot-starter-camunda-connectors |
Auto-config Spring Boot starter |
Implicaciones para el MVP¶
Decisión: ¿incluir un SDK de connectors?¶
En analysis/mvp-feature-matrix los connectors están como eliminables. Razón: los usuarios pueden escribir workers normales para integraciones.
Pero hay valor en una API unificada si se va a tener un ecosistema de integraciones.
Modelo simplificado del MVP¶
Si se incluyen connectors, replicar SOLO la primitiva outbound:
// MVP outbound connector (TypeScript example)
export interface OutboundConnector<Input, Output> {
type: string; // e.g., "http-request:1"
inputSchema: ZodSchema<Input>; // validation
execute(input: Input, context: ConnectorContext): Promise<Output>;
}
// Context provee:
// - jobContext (key, retries, deadline)
// - resolveSecrets(template: string): string
// - logger
Decisiones:
- Mantener single method (no JobCompletionListener)
- Zod (TypeScript) o equivalente para validation (no Bean Validation magic)
- Secret resolution explícita: context.resolveSecrets(template) en vez de magic
- Sin element template generator: usuario maneja UI separadamente
NO incluir inbound en MVP¶
Inbound es 10x más complejo que outbound: - Lifecycle management - Async threads - Activation conditions - Correlation logic - Failure handling strategies
En su lugar, exponer las APIs directamente:
- POST /v2/process-instances para start events
- POST /v2/messages/{name}/correlate para message events
- POST /v2/signals/{name}/broadcast para signals
Los usuarios escriben sus propios HTTP servers / Kafka consumers / etc. y llaman estas APIs. Es más código pero mucho menos complejidad en el platform.
Secret providers¶
NO incluir mecanismo de plugin. Usuarios usan env vars o sus propios systems. El MVP NO debería conocer Vault, AWS Secrets Manager, etc.
Trade-off final¶
Camunda invirtió mucho en el connector SDK porque es producto vendible. Para un MVP open-source, mantener APIs simples es más valuable que abstracciones generales. Los usuarios siempre pueden agregar wrappers.