Pular para o conteúdo principal

Steps e placeholders

A referência do que um agente pode colocar dentro de workflow.

Campos de um step​

Só existe um tipo de passo hoje: request, uma chamada HTTP de saída.

CampoObrigatórioPadrãoDescrição
typesim—request. Um tipo desconhecido rejeita o workflow inteiro — um tipo de passo que não entendemos não pode sumir em silêncio de uma cadeia que você acredita ter rodado até o fim.
urlsim—Pode ser montada inteiramente com placeholders. Checada contra o guard de SSRF depois da substituição.
methodnãoPOSTGET, POST, PUT, PATCH ou DELETE.
headersnão{}Objeto nome → valor. Veja os nomes reservados abaixo.
bodynão—String é enviada tal e qual. Objeto ou array é codificado em JSON e recebe content-type: application/json, a menos que o passo defina um content type. Exclusivo com form.
formnão—Objeto nome do campo → valor string, enviado como multipart/form-data. Um campo cujo valor inteiro é ${{email.attachments.N}} sai como parte binária de arquivo. Exclusivo com body. Um header content-type num passo com form é descartado — o boundary é nosso.
expected_statusnãoqualquer 2xxO único código de status que conta como sucesso, por exemplo 201.
labelnão—Texto livre, exibido junto ao passo na API e no back office.

Até 20 passos por workflow.

Nomes de header reservados​

Rejeitados na validação, antes de qualquer coisa rodar:

  • Host, Content-Length, Transfer-Encoding, Connection, Upgrade, Proxy-Authorization — permitiriam que um passo contradissesse a requisição que de fato enviamos.
  • Qualquer nome x-kobana-* — reservado, para que um destino nunca seja enganado lendo um header escrito pelo agente como se fosse nosso (as assinaturas de webhook vivem nesse espaço).

Nomes de header precisam ser um token HTTP válido, e nem nomes nem valores podem conter CR ou LF — é isso que impede injeção de header por uma string que o modelo compôs.

Placeholders​

Quatro namespaces, e só quatro. Qualquer outro é erro, nunca texto deixado na requisição.

Espaço em branco dentro das chaves é tolerado: ${{ secrets.TOKEN }} e ${{secrets.TOKEN}} são a mesma referência.

${{secrets.KEY}}​

Um secret do workspace. O valor é substituído na requisição de saída e redigido de tudo o que persistimos.

{ "headers": { "Authorization": "Bearer ${{secrets.FINANCE_TOKEN}}" } }

Uma referência a uma chave que não existe falha o passo antes de qualquer requisição ser enviada. A alternativa — mandar o texto literal ${{secrets.TYPO}} para um terceiro — falharia do mesmo jeito e ainda vazaria a referência para o log de outra pessoa.

${{kobana.*}}​

Valores que a plataforma expõe sobre a execução. Todos não sensíveis: ids e nomes de ambiente que você já vê na API.

ReferênciaDescrição
kobana.envdevelopment, sandbox ou production.
kobana.nowTimestamp ISO 8601, marcado quando o passo começa (não quando a execução foi criada).
kobana.app_urlURL base da plataforma neste ambiente. Útil para montar um link de volta; note que os endpoints /v1 continuam exigindo mTLS + JWT, então um destino não consegue simplesmente buscar um.
kobana.workspace_idWorkspace da execução.
kobana.workspace_slug / kobana.workspace_external_idQuando definidos no workspace.
kobana.inbox_id / kobana.inbox_emailInbox que recebeu a mensagem.
kobana.email_id / kobana.email_message_idO e-mail sendo processado.
kobana.agent_id / kobana.agent_run_idO agente e a execução que produziram o workflow.
kobana.workflow_run_idA própria execução do workflow.

A lista é aditiva: novas chaves podem aparecer, uma chave existente nunca muda de significado.

Padrão útil — fazer o registro no seu sistema apontar de volta para o e-mail:

{ "body": { "amount": 1250.5, "source_email_id": "${{kobana.email_id}}" } }

${{steps.N.response.…}}​

Saída de um passo anterior da mesma execução. N é a posição, base 0.

ReferênciaResolve para
${{steps.0.response.status}}O código de status, como string.
${{steps.0.response.body.data.id}}Um caminho dentro do corpo da resposta, interpretado como JSON quando o destino indica isso.
${{steps.0.response.body.items.0.id}}Índices de array são números simples.
${{steps.0.response.headers.location}}Um header da resposta.

Regras:

  • Só passos anteriores resolvem. Uma referência para frente falha o passo. Uma referência a um passo que falhou ou foi pulado faz o passo ficar skipped — ele nunca é enviado.
  • Um valor que resolve para objeto ou array é codificado em JSON no lugar.
  • Um valor que resolve para null falha o passo — quase sempre é caminho errado, e mandar a string null para o seu ERP é pior do que parar.
  • A busca de propriedade é só em propriedades próprias: constructor, __proto__ e afins não resolvem.

${{email.attachments.N}}​

Um anexo do e-mail que disparou o run; N é a posição dele na mensagem, a partir de 0. O arquivo nunca passa pelo modelo. O agente escreve a referência, e a plataforma lê o .eml original e coloca os bytes na requisição na hora do envio — um PDF de 5 MB custa ao agente o mesmo punhado de tokens de qualquer outro placeholder.

O que a referência vira depende de onde ela está:

OndeVira
O valor inteiro de um campo de formO arquivo em si: uma parte binária de multipart/form-data com os bytes, o nome e o content type do anexo.
Em qualquer lugar dentro de uma string — um body JSON, um header, um campo de form misturado com outro textoO base64 do arquivo. ${{email.attachments.N.base64}} diz a mesma coisa explicitamente.

Os metadados também estão disponíveis, como strings: ${{email.attachments.N.filename}}, ${{email.attachments.N.content_type}} e ${{email.attachments.N.size}} (bytes).

O content type é o que o remetente declarou, a menos que ele não tenha declarado nada útil (application/octet-stream); nesse caso é inferido pela extensão. Isso importa para um destino que confere o tipo declarado contra os magic bytes do arquivo — o upload de anexos da Kobana Finance é um deles.

Multipart, para uma API que recebe upload de arquivo. Este é o contrato do POST /attachments da Kobana Finance (file binário, title opcional, 201 no sucesso):

{
"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 uma API que só aceita 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}}"
}
}

Prefira form sempre que o destino aceitar: sem os 33% de overhead do base64 e sem uma string de vários megabytes dentro de um documento JSON.

Regras:

  • Referência a um anexo que o e-mail não tem falha o passo antes de qualquer requisição sair, com uma mensagem dizendo quantos anexos existem.
  • Um run cujo e-mail não pode mais ser lido (expurgado pela retenção) falha o passo do mesmo jeito.
  • A cópia gravada da requisição nunca carrega os bytes: um campo de arquivo em form é registrado como { "attachment": 0, "filename": …, "content_type": …, "size": … }, e o base64 dentro de um body é trocado por [attachment 0: nota.pdf, application/pdf, 12345 bytes].
  • Não peça ao agente para escrever o conteúdo do arquivo. Base64 emitido como saída do modelo custa dezenas de milhares de tokens por megabyte e sai corrompido; a referência existe exatamente para isso.

Exemplo completo​

Criar, anexar e notificar — com a notificação carregando dados das duas chamadas anteriores:

{
"result": { "supplier": "ACME", "amount": 1250.5 },
"workflow": [
{
"type": "request",
"label": "cria o pagamento",
"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": "anexa o 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 o canal do financeiro",
"method": "POST",
"url": "${{secrets.SLACK_WEBHOOK_URL}}",
"body": { "text": "Pagamento ${{steps.0.response.body.data.id}} criado em ${{kobana.now}}" }
}
]
}

Repare no segundo passo: o primeiro anexo do e-mail é enviado como arquivo, sem o agente nunca ver os bytes. E no terceiro: a URL inteira é um secret. Isso é suportado, e a URL resolvida ainda passa pelo check de SSRF antes de a conexão ser feita.

Escrevendo o prompt​

A plataforma executa o workflow; o agente é quem o escreve. Uma system message que funciona:

Ao terminar a análise do e-mail, responda com um objeto JSON contendo result (sua análise) e, quando uma ação for necessária, workflow: um array de passos. Cada passo é {"type":"request","method":…,"url":…,"headers":…,"body":…,"expected_status":…}, ou com form no lugar de body para um upload multipart. Use ${{secrets.NOME}} para credenciais — nunca escreva uma credencial literalmente —, ${{steps.N.response.body.…}} para reaproveitar um valor devolvido por um passo anterior, e ${{email.attachments.N}} como valor de um campo de form para enviar o N-ésimo anexo do e-mail — nunca escreva o conteúdo de um arquivo. Quando nenhuma ação for necessária, devolva só result.

Duas coisas que vale dizer explicitamente no seu prompt:

  • Quando não emitir workflow. Um agente em dúvida deve devolver só result. Omitir a chave é sempre seguro; chutar um endpoint não é.
  • Quais status esperar. expected_status é o que transforma um 200 {"error": …} silencioso de uma API permissiva em um passo com falha.