Steps y placeholders
La referencia de lo que un agente puede poner dentro de workflow.
Campos de un step
Hoy solo existe un tipo de paso: request, una llamada HTTP saliente.
| Campo | Obligatorio | Por defecto | Descripción |
|---|---|---|---|
type | sí | — | request. Un tipo desconocido rechaza el workflow entero — un tipo de paso que no entendemos no puede desaparecer en silencio de una cadena que usted cree que corrió completa. |
url | sí | — | Puede construirse enteramente con placeholders. Se verifica contra el guard de SSRF después de la sustitución. |
method | no | POST | GET, POST, PUT, PATCH o DELETE. |
headers | no | {} | Objeto nombre → valor. Vea los nombres reservados abajo. |
body | no | — | Un string se envía tal cual. Un objeto o array se codifica en JSON y recibe content-type: application/json, salvo que el paso defina un content type. Excluyente con form. |
form | no | — | Objeto nombre del campo → valor string, enviado como multipart/form-data. Un campo cuyo valor entero es ${{email.attachments.N}} sale como parte binaria de archivo. Excluyente con body. Una cabecera content-type en un paso con form se descarta — el boundary es nuestro. |
expected_status | no | cualquier 2xx | El único código de status que cuenta como éxito, por ejemplo 201. |
label | no | — | Texto libre, mostrado junto al paso en la API y en el back office. |
Hasta 20 pasos por workflow.
Nombres de cabecera reservados
Se rechazan en la validación, antes de que corra nada:
Host,Content-Length,Transfer-Encoding,Connection,Upgrade,Proxy-Authorization— permitirían que un paso contradijera la petición que realmente enviamos.- Cualquier nombre
x-kobana-*— reservado, para que un destino nunca sea engañado leyendo una cabecera escrita por el agente como si fuera nuestra (las firmas de webhook viven en ese espacio).
Los nombres de cabecera deben ser un token HTTP válido, y ni nombres ni valores pueden contener CR o LF — eso es lo que impide la inyección de cabeceras a través de un string que compuso el modelo.
Placeholders
Cuatro espacios de nombres, y solo cuatro. Cualquier otro es un error, nunca texto que quede en la petición.
Se tolera el espacio dentro de las llaves: ${{ secrets.TOKEN }} y ${{secrets.TOKEN}} son la misma referencia.
${{secrets.KEY}}
Un secret del workspace. El valor se sustituye en la petición saliente y se redacta de todo lo que persistimos.
{ "headers": { "Authorization": "Bearer ${{secrets.FINANCE_TOKEN}}" } }
Una referencia a una clave que no existe hace fallar el paso antes de enviar ninguna petición. La alternativa — mandar el texto literal ${{secrets.TYPO}} a un tercero — fallaría igual y encima filtraría la referencia al log de otra persona.
${{kobana.*}}
Valores que la plataforma expone sobre la ejecución. Todos no sensibles: ids y nombres de entorno que usted ya ve en la API.
| Referencia | Descripción |
|---|---|
kobana.env | development, sandbox o production. |
kobana.now | Timestamp ISO 8601, marcado al iniciar el paso (no al crearse la ejecución). |
kobana.app_url | URL base de la plataforma en este entorno. Útil para construir un enlace de vuelta; note que los endpoints /v1 siguen exigiendo mTLS + JWT, así que un destino no puede simplemente descargarlo. |
kobana.workspace_id | Workspace de la ejecución. |
kobana.workspace_slug / kobana.workspace_external_id | Cuando están definidos en el workspace. |
kobana.inbox_id / kobana.inbox_email | Inbox que recibió el mensaje. |
kobana.email_id / kobana.email_message_id | El correo que se está procesando. |
kobana.agent_id / kobana.agent_run_id | El agente y la ejecución que produjeron el workflow. |
kobana.workflow_run_id | La propia ejecución del workflow. |
La lista es aditiva: pueden aparecer claves nuevas, una clave existente nunca cambia de significado.
Patrón útil — que el registro en su sistema apunte de vuelta al correo:
{ "body": { "amount": 1250.5, "source_email_id": "${{kobana.email_id}}" } }
${{steps.N.response.…}}
Salida de un paso anterior de la misma ejecución. N es la posición, base 0.
| Referencia | Resuelve a |
|---|---|
${{steps.0.response.status}} | El código de status, como string. |
${{steps.0.response.body.data.id}} | Una ruta dentro del cuerpo de la respuesta, interpretado como JSON cuando el destino lo indica. |
${{steps.0.response.body.items.0.id}} | Los índices de array son números simples. |
${{steps.0.response.headers.location}} | Una cabecera de la respuesta. |
Reglas:
- Solo resuelven los pasos anteriores. Una referencia hacia adelante hace fallar el paso. Una referencia a un paso que falló o se saltó deja el paso
skipped— nunca se envía. - Un valor que resuelve a objeto o array se codifica en JSON en su lugar.
- Un valor que resuelve a
nullhace fallar el paso — casi siempre es una ruta equivocada, y mandar el stringnulla su ERP es peor que detenerse. - La búsqueda de propiedades es solo sobre propiedades propias:
constructor,__proto__y similares no resuelven.
${{email.attachments.N}}
Un adjunto del correo que disparó el run; N es su posición en el mensaje, desde 0. El archivo nunca pasa por el modelo. El agente escribe la referencia, y la plataforma lee el .eml original y pone los bytes en la petición al enviarla — un PDF de 5 MB le cuesta al agente el mismo puñado de tokens que cualquier otro placeholder.
En qué se convierte la referencia depende de dónde esté:
| Dónde | Se convierte en |
|---|---|
El valor entero de un campo de form | El archivo en sí: una parte binaria de multipart/form-data con los bytes, el nombre y el content type del adjunto. |
En cualquier lugar dentro de un string — un body JSON, una cabecera, un campo de form mezclado con otro texto | El base64 del archivo. ${{email.attachments.N.base64}} dice lo mismo explícitamente. |
Los metadatos también están disponibles, como strings: ${{email.attachments.N.filename}}, ${{email.attachments.N.content_type}} y ${{email.attachments.N.size}} (bytes).
El content type es el que declaró el remitente, salvo que no haya declarado nada útil (application/octet-stream); en ese caso se infiere por la extensión. Eso importa para un destino que compara el tipo declarado con los magic bytes del archivo — la subida de adjuntos de Kobana Finance es uno.
Multipart, para una API que recibe subida de archivos. Este es el contrato del POST /attachments de Kobana Finance (file binario, title opcional, 201 en éxito):
{
"type": "request",
"method": "POST",
"url": "${{secrets.FINANCE_API_URL}}/attachments",
"headers": { "Authorization": "Bearer ${{secrets.FINANCE_TOKEN}}" },
"form": { "file": "${{email.attachments.0}}", "title": "${{email.attachments.0.filename}}" },
"expected_status": 201
}
Base64, para una API que solo acepta JSON:
{
"type": "request",
"method": "POST",
"url": "https://erp.example.com/documents",
"body": {
"filename": "${{email.attachments.0.filename}}",
"content_type": "${{email.attachments.0.content_type}}",
"content": "${{email.attachments.0.base64}}"
}
}
Prefiera form siempre que el destino lo acepte: sin el 33% de sobrecarga del base64 y sin un string de varios megabytes dentro de un documento JSON.
Reglas:
- Una referencia a un adjunto que el correo no tiene falla el paso antes de que salga cualquier petición, con un mensaje que dice cuántos adjuntos hay.
- Un run cuyo correo ya no puede leerse (purgado por la retención) falla el paso de la misma forma.
- La copia guardada de la petición nunca lleva los bytes: un campo de archivo en
formse registra como{ "attachment": 0, "filename": …, "content_type": …, "size": … }, y el base64 dentro de un body se reemplaza por[attachment 0: nota.pdf, application/pdf, 12345 bytes]. - No le pida al agente que escriba el contenido del archivo. El base64 emitido como salida del modelo cuesta decenas de miles de tokens por megabyte y sale corrupto; la referencia existe justamente para eso.
Ejemplo completo
Crear, adjuntar y notificar — con la notificación llevando datos de las dos llamadas anteriores:
{
"result": { "supplier": "ACME", "amount": 1250.5 },
"workflow": [
{
"type": "request",
"label": "crea la cuenta a pagar",
"method": "POST",
"url": "https://erp.example.com/payables",
"headers": { "Authorization": "Bearer ${{secrets.FINANCE_TOKEN}}" },
"body": { "supplier": "ACME", "amount": 1250.5, "source_email_id": "${{kobana.email_id}}" },
"expected_status": 201
},
{
"type": "request",
"label": "adjunta el documento original",
"method": "POST",
"url": "https://erp.example.com/payables/${{steps.0.response.body.data.id}}/attachments",
"headers": { "Authorization": "Bearer ${{secrets.FINANCE_TOKEN}}" },
"form": { "file": "${{email.attachments.0}}", "kobana_email_id": "${{kobana.email_id}}" }
},
{
"type": "request",
"label": "notifica al canal de finanzas",
"method": "POST",
"url": "${{secrets.SLACK_WEBHOOK_URL}}",
"body": { "text": "Cuenta ${{steps.0.response.body.data.id}} creada el ${{kobana.now}}" }
}
]
}
Fíjese en el segundo paso: el primer adjunto del correo se sube como archivo, sin que el agente vea nunca los bytes. Y en el tercero: la URL entera es un secret. Está soportado, y la URL resuelta igual pasa por la verificación de SSRF antes de conectarse.
Escribiendo el prompt
La plataforma ejecuta el workflow; el agente lo escribe. Un system message que funciona:
Cuando termines de analizar el correo, responde con un objeto JSON que contenga
result(tu análisis) y, cuando haga falta una acción,workflow: un array de pasos. Cada paso es{"type":"request","method":…,"url":…,"headers":…,"body":…,"expected_status":…}, o conformen lugar debodypara una subida multipart. Usa${{secrets.NOMBRE}}para las credenciales — nunca escribas una credencial literalmente —,${{steps.N.response.body.…}}para reutilizar un valor devuelto por un paso anterior, y${{email.attachments.N}}como valor de un campo deformpara subir el N-ésimo adjunto del correo — nunca escribas el contenido de un archivo. Cuando no haga falta ninguna acción, devuelve soloresult.
Dos cosas que conviene decir explícitamente en su prompt:
- Cuándo no emitir un workflow. Un agente con dudas debe devolver solo
result. Omitir la clave siempre es seguro; adivinar un endpoint no lo es. - Qué status esperar.
expected_statuses lo que convierte un200 {"error": …}silencioso de una API permisiva en un paso fallido.