MuleSoft

Gestionarea erorilor in MuleSoft: Ghid complet de bune practici

Petru Constantin
--9 min lectura
#mulesoft#error-handling#api-resilience#fault-tolerance#integration-patterns

Gestionarea robusta a erorilor este esentiala pentru aplicatiile MuleSoft de productie. Acest ghid acopera strategii complete de gestionare a erorilor, de la pattern-uri de baza pana la tehnici avansate de rezilienta.

Concepte fundamentale de gestionare a erorilor

Sa intelegem modelul de erori al MuleSoft:

<!-- Basic try-catch-finally pattern -->
<flow name="basic-error-handling-flow">
    <http:listener config-ref="HTTP_Listener_config" path="/orders" method="POST"/>
 
    <try doc:name="Try Block">
        <!-- Main processing logic -->
        <db:insert config-ref="Database_Config" doc:name="Insert Order">
            <db:sql>INSERT INTO orders (customer_id, total) VALUES (:customerId, :total)</db:sql>
            <db:input-parameters><![CDATA[#[{
                customerId: payload.customerId,
                total: payload.total
            }]]]></db:input-parameters>
        </db:insert>
 
        <http:request
            method="POST"
            config-ref="Inventory_HTTP_Config"
            path="/reserve"
            doc:name="Reserve Inventory"/>
 
        <error-handler>
            <!-- Handle specific database errors -->
            <on-error-propagate type="DB:CONNECTIVITY" doc:name="DB Connection Error">
                <logger level="ERROR" message="Database connection failed: #[error.description]"/>
                <ee:transform doc:name="DB Error Response">
                    <ee:message>
                        <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    errorCode: "DB_CONNECTION_ERROR",
    message: "Database temporarily unavailable",
    retryAfter: 30,
    correlationId: correlationId
}]]></ee:set-payload>
                    </ee:message>
                    <ee:variables>
                        <ee:set-variable variableName="httpStatus">503</ee:set-variable>
                    </ee:variables>
                </ee:transform>
            </on-error-propagate>
 
            <!-- Handle HTTP request errors -->
            <on-error-propagate type="HTTP:CONNECTIVITY, HTTP:TIMEOUT" doc:name="HTTP Error">
                <logger level="ERROR" message="External service error: #[error.description]"/>
                <ee:transform doc:name="Service Error Response">
                    <ee:message>
                        <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    errorCode: "EXTERNAL_SERVICE_ERROR",
    message: "Inventory service unavailable",
    retryAfter: 60,
    correlationId: correlationId
}]]></ee:set-payload>
                    </ee:message>
                    <ee:variables>
                        <ee:set-variable variableName="httpStatus">503</ee:set-variable>
                    </ee:variables>
                </ee:transform>
            </on-error-propagate>
 
            <!-- Handle validation errors -->
            <on-error-propagate type="VALIDATION:*" doc:name="Validation Error">
                <ee:transform doc:name="Validation Error Response">
                    <ee:message>
                        <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    errorCode: "VALIDATION_ERROR",
    message: error.description,
    details: error.detailedDescription default [],
    correlationId: correlationId
}]]></ee:set-payload>
                    </ee:message>
                    <ee:variables>
                        <ee:set-variable variableName="httpStatus">400</ee:set-variable>
                    </ee:variables>
                </ee:transform>
            </on-error-propagate>
 
            <!-- Catch-all for unexpected errors -->
            <on-error-propagate type="ANY" doc:name="Generic Error">
                <logger level="ERROR" message="Unexpected error: #[error.errorType] - #[error.description]"/>
                <ee:transform doc:name="Generic Error Response">
                    <ee:message>
                        <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    errorCode: "INTERNAL_ERROR",
    message: "An unexpected error occurred",
    correlationId: correlationId
}]]></ee:set-payload>
                    </ee:message>
                    <ee:variables>
                        <ee:set-variable variableName="httpStatus">500</ee:set-variable>
                    </ee:variables>
                </ee:transform>
            </on-error-propagate>
        </error-handler>
    </try>
 
    <!-- Set HTTP status from variable -->
    <set-variable variableName="httpStatus" value="#[vars.httpStatus default 200]"/>
</flow>

Tipuri de erori personalizate

Defineste si arunca tipuri de erori personalizate:

<!-- Define custom error types in error-types.xml or within flow -->
<flow name="custom-error-types-flow">
    <http:listener config-ref="HTTP_Listener_config" path="/customers/{customerId}" method="GET"/>
 
    <!-- Validate customer ID format -->
    <choice doc:name="Validate Customer ID">
        <when expression="#[attributes.uriParams.customerId matches /^CUST-\d{6}$/]">
            <logger level="DEBUG" message="Valid customer ID format"/>
        </when>
        <otherwise>
            <raise-error type="APP:INVALID_CUSTOMER_ID"
                         description="Customer ID must be in format CUST-XXXXXX"/>
        </otherwise>
    </choice>
 
    <!-- Query customer -->
    <db:select config-ref="Database_Config" doc:name="Get Customer">
        <db:sql>SELECT * FROM customers WHERE customer_id = :customerId</db:sql>
        <db:input-parameters><![CDATA[#[{
            customerId: attributes.uriParams.customerId
        }]]]></db:input-parameters>
    </db:select>
 
    <!-- Check if customer exists -->
    <choice doc:name="Check Customer Exists">
        <when expression="#[sizeOf(payload) > 0]">
            <ee:transform doc:name="Transform Customer">
                <ee:message>
                    <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    customerId: payload[0].customer_id,
    name: payload[0].name,
    email: payload[0].email,
    status: payload[0].status
}]]></ee:set-payload>
                </ee:message>
            </ee:transform>
        </when>
        <otherwise>
            <raise-error type="APP:CUSTOMER_NOT_FOUND"
                         description='Customer not found: $(attributes.uriParams.customerId)'/>
        </otherwise>
    </choice>
 
    <!-- Check customer status -->
    <choice doc:name="Check Customer Status">
        <when expression="#[payload.status == 'ACTIVE']">
            <logger level="DEBUG" message="Customer is active"/>
        </when>
        <when expression="#[payload.status == 'SUSPENDED']">
            <raise-error type="APP:CUSTOMER_SUSPENDED"
                         description="Customer account is suspended"/>
        </when>
        <when expression="#[payload.status == 'CLOSED']">
            <raise-error type="APP:CUSTOMER_CLOSED"
                         description="Customer account is closed"/>
        </when>
    </choice>
 
    <error-handler>
        <on-error-propagate type="APP:INVALID_CUSTOMER_ID">
            <ee:transform>
                <ee:message>
                    <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    error: "INVALID_CUSTOMER_ID",
    message: error.description,
    httpStatus: 400
}]]></ee:set-payload>
                </ee:message>
            </ee:transform>
        </on-error-propagate>
 
        <on-error-propagate type="APP:CUSTOMER_NOT_FOUND">
            <ee:transform>
                <ee:message>
                    <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    error: "CUSTOMER_NOT_FOUND",
    message: error.description,
    httpStatus: 404
}]]></ee:set-payload>
                </ee:message>
            </ee:transform>
        </on-error-propagate>
 
        <on-error-propagate type="APP:CUSTOMER_SUSPENDED, APP:CUSTOMER_CLOSED">
            <ee:transform>
                <ee:message>
                    <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    error: error.errorType.identifier,
    message: error.description,
    httpStatus: 403
}]]></ee:set-payload>
                </ee:message>
            </ee:transform>
        </on-error-propagate>
    </error-handler>
</flow>

Handler global de erori

Centralizeaza gestionarea erorilor in intreaga aplicatie:

<!-- Global error configuration file: global-error-handler.xml -->
<configuration-properties file="error-config.yaml"/>
 
<global-property name="app.name" value="${app.name}"/>
<global-property name="app.env" value="${app.environment}"/>
 
<!-- Reusable error handler -->
<error-handler name="Global_Error_Handler">
    <!-- Authentication errors -->
    <on-error-propagate type="HTTP:UNAUTHORIZED, APIKIT:UNAUTHORIZED">
        <flow-ref name="build-auth-error-response"/>
    </on-error-propagate>
 
    <!-- Forbidden errors -->
    <on-error-propagate type="HTTP:FORBIDDEN">
        <flow-ref name="build-forbidden-error-response"/>
    </on-error-propagate>
 
    <!-- Not found errors -->
    <on-error-propagate type="APIKIT:NOT_FOUND, HTTP:NOT_FOUND">
        <flow-ref name="build-not-found-error-response"/>
    </on-error-propagate>
 
    <!-- Bad request / Validation errors -->
    <on-error-propagate type="APIKIT:BAD_REQUEST, VALIDATION:INVALID_*">
        <flow-ref name="build-validation-error-response"/>
    </on-error-propagate>
 
    <!-- Method not allowed -->
    <on-error-propagate type="APIKIT:METHOD_NOT_ALLOWED">
        <flow-ref name="build-method-not-allowed-response"/>
    </on-error-propagate>
 
    <!-- Connectivity errors (retryable) -->
    <on-error-propagate type="HTTP:CONNECTIVITY, DB:CONNECTIVITY, *:CONNECTIVITY">
        <flow-ref name="build-service-unavailable-response"/>
    </on-error-propagate>
 
    <!-- Timeout errors -->
    <on-error-propagate type="HTTP:TIMEOUT, *:TIMEOUT">
        <flow-ref name="build-timeout-error-response"/>
    </on-error-propagate>
 
    <!-- Application-specific errors -->
    <on-error-propagate type="APP:*">
        <flow-ref name="build-app-error-response"/>
    </on-error-propagate>
 
    <!-- Catch-all for unexpected errors -->
    <on-error-propagate type="ANY">
        <flow-ref name="build-internal-error-response"/>
    </on-error-propagate>
</error-handler>
 
<!-- Error response builder sub-flows -->
<sub-flow name="build-auth-error-response">
    <ee:transform>
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    apiVersion: p('api.version'),
    error: {
        code: "UNAUTHORIZED",
        message: "Authentication required",
        details: error.description,
        timestamp: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"},
        correlationId: correlationId,
        path: attributes.requestPath default "unknown"
    }
}]]></ee:set-payload>
        </ee:message>
        <ee:variables>
            <ee:set-variable variableName="httpStatus">401</ee:set-variable>
        </ee:variables>
    </ee:transform>
    <flow-ref name="log-and-track-error"/>
</sub-flow>
 
<sub-flow name="build-internal-error-response">
    <ee:transform>
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    apiVersion: p('api.version'),
    error: {
        code: "INTERNAL_SERVER_ERROR",
        message: "An unexpected error occurred",
        // Don't expose internal error details in production
        details: if (p('app.environment') != "prod") error.description else null,
        timestamp: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"},
        correlationId: correlationId,
        path: attributes.requestPath default "unknown"
    }
}]]></ee:set-payload>
        </ee:message>
        <ee:variables>
            <ee:set-variable variableName="httpStatus">500</ee:set-variable>
        </ee:variables>
    </ee:transform>
    <flow-ref name="log-and-track-error"/>
</sub-flow>
 
<sub-flow name="log-and-track-error">
    <!-- Log error details -->
    <logger level="ERROR" doc:name="Log Error">
        <ee:message><![CDATA[%dw 2.0
output application/json
---
{
    correlationId: correlationId,
    errorType: error.errorType.identifier,
    errorDescription: error.description,
    causeMessage: error.cause.message default null,
    stackTrace: if (p('app.environment') != "prod") error.stackTrace else null,
    requestPath: attributes.requestPath default "unknown",
    httpMethod: attributes.method default "unknown",
    timestamp: now()
}]]></ee:message>
    </logger>
 
    <!-- Send to error tracking system (e.g., Splunk, DataDog) -->
    <async doc:name="Async Error Tracking">
        <http:request
            method="POST"
            config-ref="Error_Tracking_HTTP_Config"
            path="/api/errors"
            doc:name="Send to Error Tracker">
            <http:body><![CDATA[#[{
                application: p('app.name'),
                environment: p('app.environment'),
                correlationId: correlationId,
                errorType: error.errorType.identifier,
                errorMessage: error.description,
                timestamp: now(),
                severity: vars.httpStatus match {
                    case 500 -> "CRITICAL"
                    case 503 -> "HIGH"
                    case _ -> "MEDIUM"
                }
            }]]]></http:body>
        </http:request>
    </async>
</sub-flow>

Strategii de retry

Implementeaza mecanisme inteligente de retry:

<flow name="retry-strategies-flow">
    <http:listener config-ref="HTTP_Listener_config" path="/process" method="POST"/>
 
    <!-- Simple retry with until-successful -->
    <until-successful
        maxRetries="${retry.maxAttempts:3}"
        millisBetweenRetries="${retry.intervalMs:2000}"
        doc:name="Retry External Call">
        <http:request
            method="POST"
            config-ref="External_Service_Config"
            path="/api/process"
            doc:name="Call External Service">
            <http:response-validator>
                <http:success-status-code-validator values="200..299"/>
            </http:response-validator>
        </http:request>
    </until-successful>
 
    <!-- Advanced retry with exponential backoff -->
    <set-variable variableName="retryCount" value="#[0]" doc:name="Init Retry Counter"/>
    <set-variable variableName="maxRetries" value="#[3]" doc:name="Set Max Retries"/>
    <set-variable variableName="baseDelayMs" value="#[1000]" doc:name="Set Base Delay"/>
 
    <until-successful maxRetries="#[vars.maxRetries]" millisBetweenRetries="#[1]">
        <try doc:name="Retry Block">
            <!-- Calculate exponential backoff delay -->
            <choice doc:name="Apply Backoff Delay">
                <when expression="#[vars.retryCount > 0]">
                    <ee:transform doc:name="Calculate Delay">
                        <ee:variables>
                            <ee:set-variable variableName="delayMs"><![CDATA[%dw 2.0
output application/java
// Exponential backoff: base * 2^retryCount + random jitter
---
vars.baseDelayMs * (2 pow vars.retryCount) + randomInt(500)]]></ee:set-variable>
                        </ee:variables>
                    </ee:transform>
                    <scripting:execute engine="groovy" doc:name="Sleep">
                        <scripting:code>Thread.sleep(vars.delayMs as Long)</scripting:code>
                    </scripting:execute>
                </when>
            </choice>
 
            <set-variable variableName="retryCount" value="#[vars.retryCount + 1]"/>
 
            <http:request
                method="POST"
                config-ref="External_Service_Config"
                path="/api/unreliable"
                doc:name="Call Unreliable Service"/>
 
            <error-handler>
                <on-error-continue type="HTTP:CONNECTIVITY, HTTP:TIMEOUT"
                                   when="#[vars.retryCount &lt; vars.maxRetries]">
                    <logger level="WARN"
                            message="Retry #[vars.retryCount] of #[vars.maxRetries] - Error: #[error.description]"/>
                    <raise-error type="MULE:RETRY_EXHAUSTED" description="Triggering retry"/>
                </on-error-continue>
 
                <on-error-propagate type="HTTP:CONNECTIVITY, HTTP:TIMEOUT">
                    <logger level="ERROR" message="All retries exhausted"/>
                    <raise-error type="APP:SERVICE_UNAVAILABLE"
                                 description="Service unavailable after all retries"/>
                </on-error-propagate>
            </error-handler>
        </try>
    </until-successful>
</flow>

Pattern-ul circuit breaker

Implementeaza circuit breaker pentru toleranta la erori:

<!-- Circuit breaker configuration using Object Store -->
<os:object-store
    name="Circuit_Breaker_Store"
    config-ref="ObjectStore_Config"
    persistent="true"
    entryTtl="300"
    entryTtlUnit="SECONDS"/>
 
<flow name="circuit-breaker-flow">
    <http:listener config-ref="HTTP_Listener_config" path="/protected/call" method="POST"/>
 
    <!-- Check circuit state -->
    <flow-ref name="check-circuit-state" doc:name="Check Circuit"/>
 
    <choice doc:name="Circuit State Router">
        <when expression="#[vars.circuitState == 'OPEN']">
            <!-- Circuit is open - fail fast -->
            <raise-error type="APP:CIRCUIT_OPEN"
                         description="Circuit breaker is OPEN. Service temporarily unavailable."/>
        </when>
        <when expression="#[vars.circuitState == 'HALF_OPEN']">
            <!-- Half-open - allow limited traffic -->
            <flow-ref name="execute-with-circuit-tracking" doc:name="Half-Open Call"/>
        </when>
        <otherwise>
            <!-- Circuit closed - normal operation -->
            <flow-ref name="execute-with-circuit-tracking" doc:name="Normal Call"/>
        </otherwise>
    </choice>
 
    <error-handler>
        <on-error-propagate type="APP:CIRCUIT_OPEN">
            <ee:transform>
                <ee:message>
                    <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    error: "SERVICE_UNAVAILABLE",
    message: "Service is temporarily unavailable. Please retry later.",
    retryAfter: vars.circuitResetTime default 60
}]]></ee:set-payload>
                </ee:message>
                <ee:variables>
                    <ee:set-variable variableName="httpStatus">503</ee:set-variable>
                </ee:variables>
            </ee:transform>
        </on-error-propagate>
    </error-handler>
</flow>
 
<sub-flow name="check-circuit-state">
    <os:retrieve
        key="circuit:failure_count"
        objectStore="Circuit_Breaker_Store"
        target="failureCount"
        doc:name="Get Failure Count">
        <os:default-value><![CDATA[0]]></os:default-value>
    </os:retrieve>
 
    <os:retrieve
        key="circuit:last_failure_time"
        objectStore="Circuit_Breaker_Store"
        target="lastFailureTime"
        doc:name="Get Last Failure Time">
        <os:default-value><![CDATA[#[0]]]></os:default-value>
    </os:retrieve>
 
    <ee:transform doc:name="Determine Circuit State">
        <ee:variables>
            <ee:set-variable variableName="circuitState"><![CDATA[%dw 2.0
output application/java
 
var failureThreshold = 5
var resetTimeoutMs = 60000
var timeSinceLastFailure = now() as Number - (vars.lastFailureTime as Number default 0)
---
if (vars.failureCount >= failureThreshold)
    if (timeSinceLastFailure > resetTimeoutMs)
        "HALF_OPEN"
    else
        "OPEN"
else
    "CLOSED"]]></ee:set-variable>
        </ee:variables>
    </ee:transform>
 
    <logger level="DEBUG" message="Circuit state: #[vars.circuitState], Failures: #[vars.failureCount]"/>
</sub-flow>
 
<sub-flow name="execute-with-circuit-tracking">
    <try doc:name="Track Circuit Status">
        <http:request
            method="POST"
            config-ref="Protected_Service_Config"
            path="/api/endpoint"
            doc:name="Call Protected Service">
            <http:response-validator>
                <http:success-status-code-validator values="200..299"/>
            </http:response-validator>
        </http:request>
 
        <!-- Success - reset failure count -->
        <os:store
            key="circuit:failure_count"
            objectStore="Circuit_Breaker_Store"
            doc:name="Reset Failure Count">
            <os:value><![CDATA[0]]></os:value>
        </os:store>
 
        <error-handler>
            <on-error-propagate type="HTTP:CONNECTIVITY, HTTP:TIMEOUT, HTTP:SERVICE_UNAVAILABLE">
                <!-- Increment failure count -->
                <os:retrieve
                    key="circuit:failure_count"
                    objectStore="Circuit_Breaker_Store"
                    target="currentFailures"
                    doc:name="Get Current Failures">
                    <os:default-value><![CDATA[0]]></os:default-value>
                </os:retrieve>
 
                <os:store
                    key="circuit:failure_count"
                    objectStore="Circuit_Breaker_Store"
                    doc:name="Increment Failures">
                    <os:value><![CDATA[#[vars.currentFailures + 1]]]></os:value>
                </os:store>
 
                <os:store
                    key="circuit:last_failure_time"
                    objectStore="Circuit_Breaker_Store"
                    doc:name="Update Last Failure Time">
                    <os:value><![CDATA[#[now() as Number]]]></os:value>
                </os:store>
 
                <logger level="WARN"
                        message="Circuit failure recorded. Count: #[vars.currentFailures + 1]"/>
            </on-error-propagate>
        </error-handler>
    </try>
</sub-flow>

Pattern-ul Dead Letter Queue

Gestioneaza mesajele esuate in mod controlat:

<flow name="message-processing-with-dlq">
    <jms:listener config-ref="JMS_Config" destination="orders.queue" doc:name="Listen for Orders"/>
 
    <set-variable variableName="originalPayload" value="#[payload]" doc:name="Store Original"/>
    <set-variable variableName="messageId" value="#[attributes.messageId]" doc:name="Store Message ID"/>
 
    <try doc:name="Process Message">
        <!-- Validate message -->
        <flow-ref name="validate-order-message" doc:name="Validate"/>
 
        <!-- Process order -->
        <flow-ref name="process-order" doc:name="Process"/>
 
        <error-handler>
            <!-- Transient errors - retry -->
            <on-error-continue type="HTTP:CONNECTIVITY, DB:CONNECTIVITY"
                               when="#[vars.retryCount default 0 &lt; 3]">
                <set-variable variableName="retryCount" value="#[(vars.retryCount default 0) + 1]"/>
                <logger level="WARN" message="Transient error, scheduling retry #[vars.retryCount]"/>
 
                <!-- Requeue with delay -->
                <jms:publish config-ref="JMS_Config" destination="orders.retry.queue" doc:name="Requeue">
                    <jms:message>
                        <jms:body><![CDATA[#[vars.originalPayload]]]></jms:body>
                        <jms:properties><![CDATA[#[{
                            retryCount: vars.retryCount,
                            originalMessageId: vars.messageId,
                            lastError: error.description
                        }]]]></jms:properties>
                    </jms:message>
                </jms:publish>
            </on-error-continue>
 
            <!-- Permanent failures - send to DLQ -->
            <on-error-continue type="ANY">
                <logger level="ERROR" message="Permanent failure, sending to DLQ: #[error.description]"/>
 
                <ee:transform doc:name="Build DLQ Message">
                    <ee:message>
                        <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    originalMessage: vars.originalPayload,
    error: {
        type: error.errorType.identifier,
        description: error.description,
        timestamp: now()
    },
    metadata: {
        messageId: vars.messageId,
        retryCount: vars.retryCount default 0,
        source: "orders.queue",
        processedAt: now()
    }
}]]></ee:set-payload>
                    </ee:message>
                </ee:transform>
 
                <jms:publish config-ref="JMS_Config" destination="orders.dlq" doc:name="Send to DLQ"/>
 
                <!-- Alert on DLQ -->
                <async doc:name="Alert Team">
                    <http:request
                        method="POST"
                        config-ref="Slack_Webhook_Config"
                        path="/services/webhook"
                        doc:name="Slack Alert">
                        <http:body><![CDATA[#[{
                            text: "⚠️ Message sent to DLQ",
                            attachments: [{
                                color: "danger",
                                fields: [
                                    {title: "Message ID", value: vars.messageId, short: true},
                                    {title: "Error", value: error.description, short: false}
                                ]
                            }]
                        }]]]></http:body>
                    </http:request>
                </async>
            </on-error-continue>
        </error-handler>
    </try>
</flow>

Concluzie

Gestionarea eficienta a erorilor in MuleSoft necesita o abordare pe mai multe niveluri: handlere specifice pentru erorile cunoscute, handlere globale pentru consistenta, strategii de retry pentru erorile tranzitorii si circuit breaker pentru protectia sistemului. Implementeaza logging si alerte comprehensive pentru a mentine vizibilitatea. Aceste pattern-uri asigura ca integrarile tale raman reziliente si usor de intretinut in productie.


Sistemul tau AI e conform cu EU AI Act? Evaluare gratuita de risc - afla in 2 minute →

Ai nevoie de ajutor cu conformitatea EU AI Act sau securitatea AI?

Programeaza o consultatie gratuita de 30 de minute. Fara obligatii.

Programeaza un Apel

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.