Saltar a contenido

Connector Sdk Architecture

El Connector SDK de Camunda 8 tiene dos primitivas: OutboundConnectorFunction (single execute() method, stateless) y InboundConnectorExecutable (lifecycle activate/deactivate, long-running, async). Outbound corre como job worker. Inbound corre continuamente y correlaciona eventos a procesos. El runtime maneja secret resolution + validation + deserialization en context.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:

  1. Deserialize: variables del job (JSON/MessagePack) a un POJO Java
  2. Replace secrets: {{secrets.MY_TOKEN}} se reemplaza por el valor real
  3. 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:

  1. Lee las anotaciones del POJO de input del connector
  2. Genera estructura JSON con fields, validations, defaults
  3. Output: template.json upload-able a Web Modeler
  4. 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.