Nesse artigo vou demonstrar como pensar em *as-Code e te entregar exatamente os arquivos yaml que você precisará especificar para o seu negócio, toda arquitetura e ferramentas necessárias eu proverei para você apenas usar.
Vamos pegar um chatbot de agendamento de consultas odontológicas para 1 dentista.
ADENDO
O aluno irá receber skills e prompts para fazer a IA retirar as configurações já existentes nos seus sistemas e converter para AllasCode. Ou seja, o aluno não irá aprender a implementar um conceito, irá aprender como funciona e como acontece a extração de cada valor escrito no seu código. Ao final do curso o aluno possuirá um sistema lançado em produção e sem ter nenhum código escrito diretamente, esse é meu framework AllasCode Após 6 meses que eu liberar para os alunos, cada camada, eu liberarei Open Source, porém irei publicar as provas de conceito ao início de cada módulo.
Para iniciar qualquer processo no nosso sistema precisamos de um canal para receber as mensagens do WhatsApp (Whatsmeow, EvolutionAPI, Whatserl *minha implementação própria baseada no Whatsmeow). Para qualquer sistema que você for criar, você pode começar com a modelagem do banco (schemas) ou com a definição do(s) canal(is) de entrada.
Toda API que eu faço é um Event-driven MCP Server, cada Tool eu crio as interfaces de stdio, websocket, http e filas, foi algo natural para mim, pois todas minhas apis também tinham interface de filas, mantendo o padrão eu criei o MCP-as-Code, que também fará parte do curso.
Para recebermos as mensagens do WhatsApp, precisamos de uma API e uma rota para receber as mensagens. A única rota que precisamos é uma rota POST /webhook.
0. API
Para esse exemplo irei omitir a parte de autenticação e idempotência, escalabilidade, resiliência, EventSourcing, etc. Quando chegarmos mais para frente iremos adicionar mais propriedades para essa configuração.
Qual o ganho de usar o API-as-Code?
Eu vou te dar um exemplo de API-as-Code onde a mesma é executada em: Typescript, Go, Python, Rust e Erlang.
Tá bom?
apiVersion: fas.dev/v1
kind: ApiAsCode
metadata:
name: whatsapp-webhook-only
spec:
server:
port: 8888
basePath: "/"
routes:
- name: whatsapp-webhook
method: POST
path: "/webhook"
input:
contentType: "application/json"
schemaRef: "schema.as-code.yaml#/schemas/WhatsAppWebhookPayload"
Esse é apenas um gostinho do poder da Programação declarativa.
Abaixo vou deixar o exemplo funcional de 10 camadas as-Code que você irá aprender seus conceitos e como traduzir seus pensamentos em configurações.
0 programação
1.1) schema.as-code.yaml (contrato mínimo)
apiVersion: fas.dev/v1
kind: SchemaAsCode
metadata:
name: clinic-schemas
spec:
schemas:
WhatsAppWebhookPayload:
type: object
properties:
message:
type: object
properties:
id: { type: string }
from: { type: string }
text: { type: string }
timestamp: { type: string }
required: [id, from, text, timestamp]
required: [message]
WhatsAppMessageReceived:
type: object
properties:
messageId: { type: string }
from: { type: string }
text: { type: string }
timestamp: { type: string }
required: [messageId, from, text, timestamp]
Appointment:
type: object
properties:
id: { type: string }
customerPhoneE164: { type: string }
startsAt: { type: string, format: date-time }
durationMinutes: { type: integer }
status: { type: string }
required: [id, customerPhoneE164, startsAt, durationMinutes, status]
BehaviorInputs:
AppointmentCreateFromTextInput:
type: object
properties:
messageId: { type: string }
from: { type: string }
text: { type: string }
timestamp: { type: string }
required: [messageId, from, text, timestamp]
BehaviorOutputs:
AppointmentCreateOutput:
type: object
properties:
appointmentId: { type: string }
status: { type: string }
required: [appointmentId, status]
Faltam (para completar o exemplo):
- Normalização real do payload (WhatsApp Cloud vs Evolution vs Whatserl)
-
correlation_id/causation_id/idempotency_keyno envelope - Schema de "erro canônico" (para respostas consistentes)
- Schema de "evidência/audit" (se for Compliance-evidence-first)
2) channel.as-code.yaml (mínimo do canal)
apiVersion: fas.dev/v1
kind: ChannelAsCode
metadata:
name: whatsapp-first-clinic
owner: student
spec:
queueBackend:
provider: rabbitmq
exchange:
name: "fas.whatsapp"
type: topic
durable: true
naming:
prefix: "fas"
environment: "dev"
rules:
- pattern: "{prefix}.{env}.{domain}.{entity}.{topic}"
topics:
events: "events"
dlq: "dlq"
retry: "retry"
Faltam:
-
delivery(timeout/retry/backoff) - DLQ/retry de verdade (roteamento + políticas por erro)
- Idempotência no canal (ou no API layer)
- Observabilidade do canal (métricas, tracing, lag)
3) api.as-code.yaml (webhook → evento canônico)
apiVersion: fas.dev/v1
kind: ApiAsCode
metadata:
name: whatsapp-webhook-only
spec:
server:
port: 8888
basePath: "/"
routes:
- name: whatsapp-webhook
method: POST
path: "/webhook"
input:
contentType: "application/json"
schemaRef: "schema.as-code.yaml#/schemas/WhatsAppWebhookPayload"
emits:
- topic: "fas.dev.crm.whatsapp.events"
eventType: "WhatsAppMessageReceived"
schemaRef: "schema.as-code.yaml#/schemas/WhatsAppMessageReceived"
map:
messageId: "{{body.message.id}}"
from: "{{body.message.from}}"
text: "{{body.message.text}}"
timestamp: "{{body.message.timestamp}}"
response:
status: 200
body:
ok: true
Faltam:
- Autenticação/assinatura (HMAC/verify token)
- Idempotência (header/body → idempotency key)
- Rate limit + proteção contra replay
- Persistência de "raw payload" (controlada por policy/compliance)
- Modo "respond immediately vs async" (aqui é async por evento)
4) tool.as-code.yaml (1 tool)
Um tool só que encapsula o efeito colateral "criar agendamento + notificar" (o runtime por baixo faz DB + WhatsApp, mas o aluno não mexe nisso).
apiVersion: fas.dev/v1
kind: ToolAsCode
metadata:
name: clinic-tools
owner: student
spec:
tools:
- name: clinic.appointment.create
description: "Cria o agendamento (persistência) e notifica o cliente."
inputSchema:
type: object
properties:
customerPhoneE164: { type: string }
startsAt: { type: string, format: date-time }
durationMinutes: { type: integer }
messageId: { type: string }
required: [customerPhoneE164, startsAt, durationMinutes, messageId]
outputSchema:
type: object
properties:
appointmentId: { type: string }
status: { type: string }
required: [appointmentId, status]
Faltam:
- Separar efeitos (DB vs WhatsApp) em tools diferentes (quando você quiser granularidade real)
- Contrato de erro (ex.: "slot indisponível", "fora do horário", "falha no envio")
- Idempotência no tool (messageId como chave de dedupe)
- Retries por tipo de erro (transiente vs permanente)
5) policy.as-code.yaml (1 policy)
apiVersion: fas.dev/v1
kind: PolicyAsCode
metadata:
name: clinic-policy
spec:
policies:
Clinic.OfficeHours:
description: "Agendamento só pode acontecer dentro do horário de funcionamento."
rules:
timezone: "America/Sao_Paulo"
weekly:
- day: MONDAY
open: "08:00"
close: "18:00"
- day: TUESDAY
open: "08:00"
close: "18:00"
- day: WEDNESDAY
open: "08:00"
close: "18:00"
- day: THURSDAY
open: "08:00"
close: "18:00"
- day: FRIDAY
open: "08:00"
close: "18:00"
Faltam:
- Feriados/pausas (almoço), exceções e agenda do dentista
- Política de "slot disponível" (conflito com agenda)
- Cancelamento/remarcação + janela mínima
- Política de confirmação contextual (para ações de impacto)
6) compliance.as-code.yaml (1 compliance)
apiVersion: fas.dev/v1
kind: ComplianceAsCode
metadata:
name: clinic-compliance
spec:
controls:
Audit.DecisionLog:
description: "Registrar evidência mínima de decisão/execução."
required:
- eventType
- correlationId
- evidence
sink:
type: eventStore
topic: "fas.dev.infra.audit.events"
Faltam:
- PII minimization (o que pode/ não pode ir para audit)
- Retenção (TTL) e export (LGPD)
- Trilhas por "ação crítica" (cancelar/pagar)
- Evidência de "quem/qual agente/qual versão do spec"
7) state.as-code.yaml (estado mínimo)
apiVersion: fas.dev/v1
kind: StateAsCode
metadata:
name: clinic-state
spec:
states:
ConversationState:
lifecycle:
- name: "IDLE"
- name: "AWAITING_DATE"
- name: "AWAITING_CONFIRMATION"
defaults:
state: "IDLE"
Faltam:
- Estado por usuário (keyed by
from) - TTL de sessão (24h WhatsApp window)
- Migração de estado (State-as-Code migrations)
- Replay/reprocess de eventos com estado materializado
8) entity-agent.as-code.yaml (1 agent)
Um agente só: ClinicAgent (ele "entende" agendamento; Customer vira detalhe implícito pelo telefone).
apiVersion: fas.dev/v1
kind: EntityAgentAsCode
metadata:
name: clinic-agent
spec:
agents:
ClinicAgent:
entity: Appointment
schemaRef: "schema.as-code.yaml#/schemas/Appointment"
stateRef: "state.as-code.yaml#/states/ConversationState"
tools:
- "tool.as-code.yaml#/tools/clinic.appointment.create"
policies:
- "policy.as-code.yaml#/policies/Clinic.OfficeHours"
compliance:
- "compliance.as-code.yaml#/controls/Audit.DecisionLog"
behaviors:
- "atomic-behavior-agent.as-code.yaml#/agents/Appointment.CreateFromText"
Faltam:
- Invariants-as-Code (futuro, idempotência, confirmação)
- Tool permissions (allow/deny por ambiente)
- Observabilidade por agente (last decision, last error, etc)
- Multi-intent (cancelar, remarcar, perguntar preço, etc)
9) atomic-behavior-agent.as-code.yaml (1 behavior)
Aqui eu mantive "steps" bem curtos. Repara que eu uso run: Extract como capacidade do runtime (não conta como tool). O único tool é o clinic.appointment.create.
apiVersion: fas.dev/v1
kind: AtomicBehaviorAgentAsCode
metadata:
name: clinic-behaviors
spec:
agents:
Appointment.CreateFromText:
description: "Extrai data/hora do texto e cria agendamento."
inputSchemaRef: "schema.as-code.yaml#/schemas/BehaviorInputs/AppointmentCreateFromTextInput"
outputSchemaRef: "schema.as-code.yaml#/schemas/BehaviorOutputs/AppointmentCreateOutput"
steps:
- run: Extract
description: "Extrai startsAt e durationMinutes do texto."
from:
text: "{{input.text}}"
into:
startsAt: "{{extract.datetime}}"
durationMinutes: "{{extract.int | default(30)}}"
- run: PolicyCheck
ref: "policy.as-code.yaml#/policies/Clinic.OfficeHours"
with:
datetime: "{{startsAt}}"
- run: Tool
ref: "tool.as-code.yaml#/tools/clinic.appointment.create"
with:
customerPhoneE164: "{{input.from}}"
startsAt: "{{startsAt}}"
durationMinutes: "{{durationMinutes}}"
messageId: "{{input.messageId}}"
saveAs: result
- run: Compliance
ref: "compliance.as-code.yaml#/controls/Audit.DecisionLog"
with:
eventType: "AppointmentCreated"
correlationId: "{{input.messageId}}"
evidence:
from: "{{input.from}}"
startsAt: "{{startsAt}}"
appointmentId: "{{result.appointmentId}}"
returns:
appointmentId: "{{result.appointmentId}}"
status: "{{result.status}}"
Faltam:
- Specification-as-Code (perguntas de clarificação quando
startsAtnão vem) - Invariants-as-Code (agendamento no futuro, idempotência, confirmação)
- Tratamento de erro (slot ocupado, fora do horário, ambíguo)
- "Confirmation step" (antes de criar de fato) — ação de impacto
10) flow.as-code.yaml (1 flow)
Um flow único que roteia o evento de entrada para o agente.
apiVersion: fas.dev/v1
kind: FlowAsCode
metadata:
name: whatsapp-inbound-routing
spec:
triggers:
- name: on-whatsapp-message
type: event
topic: "fas.dev.crm.whatsapp.events"
eventType: "WhatsAppMessageReceived"
action:
agentRef: "entity-agent.as-code.yaml#/agents/ClinicAgent"
behaviorRef: "atomic-behavior-agent.as-code.yaml#/agents/Appointment.CreateFromText"
inputMap:
messageId: "{{event.messageId}}"
from: "{{event.from}}"
text: "{{event.text}}"
timestamp: "{{event.timestamp}}"
Faltam:
- Retry/DLQ no nível do flow (transiente vs permanente)
- Idempotência do flow (dedupe por messageId)
- Roteamento por intenção (create vs cancel vs info)
- "Flow-as-Code cron" (ex.: lembrete 24h) — você pediu 1 flow, então ficou fora
Além dessas 10, você só precisará de mais 2 para ter um chatbot completo:
11. Intent-as-Code
12. Specification-as-Code
CTA
Minha arquitetura ainda possui muitas outras especificações as-Code, mas eu quis deixar apenas o necessário para o funcionamento do chatbot.

Top comments (0)