Pular para o conteúdo principal

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 tabela data conterá uuid (o UUID do contato), contact (o contato completo) e changes (uma lista de tabelas, cada uma com as chaves field, old e new).
  • "delivery_errors": Uma mensagem falhou na entrega com um código de erro ao qual seu app se inscreveu via turn.app.set_delivery_error_subscriptions(). A tabela data contém uma lista errors onde cada item inclui code, status, message_id, contact, timestamp e upstream_error (o erro bruto do Meta).
  • "http_request": A URL de webhook pública do seu app recebeu uma requisição. A tabela data contém method, request_path, path_info, query_string, req_headers, body_params, query_params e params (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

  1. 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
  2. Webhook Externo Chega: Mais tarde, seu provedor de pagamento envia um webhook para o endpoint HTTP do seu app. O manipulador http_request do seu app o analisa.

  3. App Retoma a Jornada: Dentro do manipulador http_request, você usa turn.leases.send_input() com o chat_uuid original 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.

Skills de Agentes IA

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
Consultando o contato

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
Chamar com override = true encerra a jornada atual

Quando 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
Não use @ dentro dos colchetes

Tanto @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