Conceitos Fundamentais
A Função on_event
A função on_event no seu arquivo .lua principal é o coração da sua aplicação. É o ponto de entrada único que recebe e roteia todos os eventos da plataforma Turn.io. Eventos comuns incluem:
"install": Seu app está sendo instalado."uninstall": Seu app está sendo desinstalado."upgrade": Seu app está sendo atualizado de uma versão mais antiga. Veja Atualizações de Apps para detalhes."downgrade": Seu app está sendo revertido para uma versão mais antiga. Veja Atualizações de Apps para detalhes."config_changed": A configuração do seu app foi atualizada. Este é um bom momento para reavaliar configurações ou subscrições."contact_fields_changed": Um contato foi atualizado, e um ou mais dos campos alterados são campos aos quais seu app se inscreveu. A tabeladataconteráuuid(o UUID do contato),contact(o contato completo) echanges(uma lista de tabelas, cada uma com as chavesfield,oldenew)."delivery_errors": Uma mensagem falhou na entrega com um código de erro ao qual seu app se inscreveu viaturn.app.set_delivery_error_subscriptions(). A tabeladatacontém uma listaerrorsonde cada item incluicode,status,message_id,contact,timestampeupstream_error(o erro bruto do Meta)."http_request": A URL de webhook pública do seu app recebeu uma requisição. A tabeladatacontémmethod,request_path,path_info,query_string,req_headers,body_params,query_paramseparams(query + body params combinados)."journey_event": Uma Jornada chamou seu app.
Sua função receberá quatro argumentos:
app: Uma tabela contendo a configuração da instância do seu app, incluindo seu UUID único.number: Uma tabela com informações sobre o número onde o app está instalado.event: Uma string identificando o tipo do evento.data: Uma tabela contendo dados específicos daquele evento.
Uma funcionalidade central dos apps é a subscrição a alterações em campos de contato específicos. Você pode definir essas subscrições usando a nova API turn.app, geralmente em resposta aos eventos install e config_changed.
local App = {}
local turn = require("turn")
function App.on_event(app, number, event, data)
if event == "install" or event == "config_changed" then
-- Na instalação ou alteração de configuração, (re)defina os campos de contato que queremos observar.
-- Isso usa a nova API `turn.app`.
turn.app.set_contact_subscriptions({"name", "surname", "age"})
return true
elseif event == "contact_fields_changed" then
-- Reage a campos específicos e subscritos que foram atualizados em um contato.
local contact_uuid = data.uuid
local changes = data.changes -- Uma lista de tabelas com chaves field, old, new
local _updated_contact = data.contact -- O contato completo com todos os campos, incluindo os alterados
for _, change in ipairs(changes) do
turn.logger.info(
"Contato " .. contact_uuid .. " campo '" .. change.field .. "' alterado de '" ..
tostring(change.old) .. "' para '" .. tostring(change.new) .. "'"
)
end
return true
elseif event == "delivery_errors" then
-- Reage a falhas de entrega de mensagens para códigos de erro subscritos.
for _, error in ipairs(data.errors) do
turn.logger.info(
"Mensagem " .. error.message_id .. " falhou com código " .. error.code ..
" para contato " .. error.contact.uuid
)
-- error.upstream_error contém o erro bruto da API do Meta.
-- Veja: https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes
end
return true
elseif event == "http_request" then
-- Lida com uma requisição HTTP recebida para o endpoint único do seu app
return true, { status = 200, body = "Olá do meu app!" }
else
-- É uma boa prática lidar com eventos não tratados como `uninstall` ou o `contact_changed` geral.
turn.logger.warning("Evento não tratado recebido: " .. event)
return false
end
end
return App
Integrando com Jornadas
Uma das funcionalidades mais poderosas dos Apps Lua é sua capacidade de integrar diretamente com Jornadas usando o bloco app(). Isso permite adicionar lógica personalizada, cálculos ou chamadas de API bem no meio de uma conversa.
Chamando um App a partir de uma Jornada
Na sua Jornada você pode chamar uma função de app assim:
card GetWeather do
# Chama a função 'get_forecast' no 'weather_app'
weather_data = app("weather_app", "get_forecast", ["Cape Town"])
# O resultado está disponível na variável 'weather_data'
text("O clima em Cape Town é: @(weather_data.result.temperature)°C")
end
Isso aciona um journey_event no seu app Lua.
Lidando com um journey_event
Seu app deve lidar com o journey_event e pode controlar o fluxo da Jornada pelo seu valor de retorno.
Fluxo Síncrono: continue
Para operações que completam instantaneamente, retorne "continue" junto com o resultado. A Jornada prosseguirá para o próximo bloco sem pausar.
- Casos de uso: Validação de dados, cálculos simples, formatação de texto.
- Assinatura de retorno:
return "continue", result_table
-- Na sua função on_event
elseif event == "journey_event" and data.function_name == "add" then
local sum = tonumber(data.args[1]) + tonumber(data.args[2])
-- A Jornada continua imediatamente com o resultado
return "continue", { value = sum }
end
Fluxo Assíncrono: wait e turn.leases
Para tarefas de longa duração, como aguardar um webhook de confirmação de pagamento, você pode dizer à Jornada para pausar retornando "wait".
A Jornada permanecerá pausada até que seu app explicitamente a retome enviando dados para seu lease. Um lease é uma retenção temporária do estado da Jornada, identificado pelo chat_uuid.
- Casos de uso: Aguardar webhooks, aprovações humanas ou atrasos temporizados.
- Assinatura de retorno:
return "wait"
Exemplo de Fluxo de Trabalho: Aguardando um Webhook de Pagamento
-
Jornada Inicia e Aguarda: A Jornada chama seu app, que inicia um pagamento e diz à Jornada para aguardar.
-- manipulador de journey_event
if data.function_name == "waitForPayment" then
-- O app pode chamar uma API de pagamento externa aqui
-- ...
-- Agora, diga à Jornada para pausar
return "wait"
end -
Webhook Externo Chega: Mais tarde, seu provedor de pagamento envia um webhook para o endpoint HTTP do seu app. O manipulador
http_requestdo seu app o analisa. -
App Retoma a Jornada: Dentro do manipulador
http_request, você usaturn.leases.send_input()com ochat_uuidoriginal para retomar a Jornada correta e entregar o resultado.-- manipulador de http_request
-- Assuma que você obteve o chat_uuid dos metadados do webhook
local chat_uuid = webhook_payload.metadata.chat_uuid
local result_data = {
payment_confirmed = true,
transaction_id = "txn_123"
}
turn.leases.send_input(chat_uuid, result_data)
A Jornada recebe os result_data e automaticamente retoma a execução.
Apps Lua também podem expor skills—funções que agentes IA chamam durante conversas para recuperar dados ou executar ações. Skills usam um formato LDoc simples e têm acesso à API Lua completa do Turn. Veja Skills de Agentes IA para detalhes.
Entendendo os Dados de journey_event
O parâmetro data no seu manipulador journey_event contém contexto importante:
elseif event == "journey_event" then
-- data contém:
-- data.function_name - A função sendo chamada (ex: "calculate", "fetch_data")
-- data.args - Array de argumentos passados da Jornada (já avaliados)
-- data.chat_uuid - UUID do chat (pode ser nil no simulador)
-- data.contact_uuid - UUID do contato (pode ser nil no simulador)
local function_name = data.function_name
local args = data.args or {}
local chat_uuid = data.chat_uuid -- Armazene isso para operações assíncronas!
if function_name == "process_order" then
local order_id = args[1]
local amount = args[2]
-- Processa e retorna resultado
return "continue", { order_id = order_id, status = "processed" }
end
end
Use data.contact_uuid com turn.contacts.get() para consultar o contato diretamente do banco de dados, sem passar pelo Elasticsearch. Isso evita qualquer preocupação com consistência eventual:
local contact, found = turn.contacts.get(data.contact_uuid)
if found then
local name = contact.details.name
end
Tratamento de Erros em journey_event
O tratamento adequado de erros garante que sua Jornada possa lidar graciosamente com problemas esperados e inesperados:
elseif event == "journey_event" then
local function_name = data.function_name
if function_name == "validate_input" then
local input = data.args[1]
-- Erro de lógica de negócio (esperado, Jornada continua)
if not input or input == "" then
return "continue", { valid = false, error = "Entrada não pode estar vazia" }
end
-- Caso de sucesso
return "continue", { valid = true, processed_input = string.upper(input) }
elseif function_name == "external_api_call" then
-- Erro de sistema (inesperado, Jornada deve lidar com caminho de erro)
local response, status = turn.http.request({
url = "https://api.example.com/data",
method = "GET"
})
if status ~= 200 then
return "error", "Chamada de API falhou com status: " .. status
end
return "continue", turn.json.decode(response)
else
-- Função desconhecida
return "continue", {
success = false,
message = "Função desconhecida: " .. function_name
}
end
end
Roteando Contatos para Outras Jornadas
Seu app pode rotear um contato para uma jornada diferente diretamente de um manipulador journey_event usando turn.journeys.start(). Isso é útil quando seu app precisa decidir em qual jornada um contato deve entrar com base em lógica externa (ex: testes A/B, verificações de elegibilidade, suporte por níveis).
elseif event == "journey_event" and data.function_name == "route_contact" then
local chat_uuid = data.chat_uuid
local contact_uuid = data.contact_uuid
-- Determina qual jornada iniciar (ex: da configuração do app ou de uma chamada de API)
local target_journey_uuid = determine_target_journey()
-- Opcionalmente atualiza campos do contato antes de rotear
local contact, found = turn.contacts.get(contact_uuid)
if found then
turn.contacts.update_contact_details(contact, {
routed_to = "experiment_arm_a"
})
end
-- Inicia a jornada alvo, substituindo a atual
local result, ok = turn.journeys.start(chat_uuid, target_journey_uuid, { override = true })
if ok then
return "continue", { routed = true }
else
return "continue", { routed = false, error = result }
end
end
override = true encerra a jornada atualQuando você chama turn.journeys.start() com override = true de dentro de um manipulador journey_event, o lease da jornada chamadora é liberado imediatamente e a jornada chamadora é encerrada. A nova jornada inicia de forma assíncrona. Nenhum estado é passado da jornada chamadora para a nova — use campos de contato ou turn.leases.update_metadata() se precisar carregar dados entre elas.
Passando Configuração para Jornadas
Apps podem escrever dados de configuração que jornadas leem em tempo de execução usando tabelas de dados. Isso evita codificar valores diretamente nos notebooks de jornada.
Escrevendo dados do seu app
Use turn.data.dictionary para armazenar configuração chave-valor:
-- Escreve configuração de experimento que a jornada pode ler
turn.data.dictionary.set("evidential", "experiment_id", "exp_12345")
turn.data.dictionary.set("evidential", "arm_a_journey", "d58b0319-eb3f-4884-b43b-4ef0b7c37e1d")
Lendo dados nas jornadas
Jornadas acessam valores de tabelas de dados via expressões. Valores de dicionário ficam disponíveis sob o namespace que você escolheu:
card RouteContact do
text("Seu experimento: @(evidential.experiment_id)")
end
Acesso dinâmico a chaves
Expressões de jornada suportam acesso dinâmico a chaves de mapas usando a sintaxe @(mapa[chave]). Isso permite buscar valores usando campos de contato ou outras variáveis em tempo de execução:
card RouteContact do
# Busca o UUID da jornada para o braço atribuído a este contato
target_journey = "@(arm_journeys[contact.assigned_arm])"
text("Roteando para: @target_journey")
end
@ dentro dos colchetesTanto @mapa[chave] quanto @(mapa[chave]) funcionam. Porém, não adicione um @ extra dentro dos colchetes — @mapa[@chave] falhará porque o @ externo já abre o contexto da expressão.
Gerenciando Jornadas Criadas pelo App
Se seu app cria jornadas programaticamente (ex: via turn.manifest.install()), use mapeamentos de jornada para rastrear os UUIDs das jornadas instaladas:
-- Após instalar uma jornada
local result, ok = turn.manifest.install(manifest)
if ok then
-- O mapeamento é definido automaticamente pelo manifest.install()
-- Recupere-o depois:
local journey_uuid = turn.app.get_journey_mapping("my_routing_journey")
end
Isso evita que jornadas órfãs se acumulem quando a configuração muda. Sempre verifique se existe um mapeamento antes de criar uma nova jornada:
local existing_uuid = turn.app.get_journey_mapping("my_journey")
if existing_uuid then
-- Jornada já existe, atualize a configuração em vez de criar uma nova
turn.data.dictionary.set("my_namespace", "setting", new_value)
else
-- Primeira vez: crie a jornada
local result, ok = turn.manifest.install(manifest)
end