En esta sección
Guías / Funciones y flujos

Funciones y flujos

Haz que tu Met llame a tu propio backend: una Función con handler HTTP o un flujo, vinculada al Met.

Actualizada el Ver .md

Una Función IA es UNA herramienta que el Met puede llamar (scopes functions:read / functions:write). Cuando el Met decide usarla, recolecta los argumentos según tus parameters y dispara el handler que definiste. Así conectas tu propio backend a la conversación.

Parámetros: el schema de la tool

Cada FunctionParameter entra al JSON schema que ve el LLM. Solo name es obligatorio:

const parameters = [
  { name: 'sku', description: 'Código de producto', required: true },
  { name: 'moneda', description: 'Moneda del precio', enum_values: ['COP', 'USD'] },
];

Campos disponibles: name, description, required, type ('string' | 'number' | 'boolean'), enum_values (lista cerrada; null/[] = libre) y save_to (el field_key del contacto donde persistir el valor recolectado, o null para no guardar).

Una Función sin handler queda inerte (no invocable). Tienes dos caminos para dárselo.

A) Handler HTTP directo

El camino más corto para pegarle a tu endpoint: pasa http. Los argumentos que el Met recolecta viajan como body JSON a tu URL.

const fn = await met.functions.create({
  name: 'consultar_precio',
  description: 'Consulta el precio vigente de un SKU en nuestro backend',
  prompt: 'Úsala cuando el cliente pregunte por el precio de un producto.',
  parameters,
  http: {
    url: 'https://api.tu-empresa.com/precios',
    method: 'POST',
    auth_workspace_variable: 'api_token',
    sign_secret_variable: 'firma_precios',
    timeout_ms: 15000,
  },
});

Los secretos NO van en crudo. auth_workspace_variable (Bearer token, se envía como Authorization) y sign_secret_variable (secreto de firma) se referencian por nombre de variable del workspace. Créalas antes con met.variables.set('api_token', 'sk_live_…', { encrypted: true }) — ver Variables del workspace.

Meteor firma toda llamada con X-Met-Signature (el mismo HMAC de los eventos salientes), y ya no es opcional: si no configuras sign_secret_variable, Meteor genera un secreto para tu workspace en la variable MET_TOOL_SIGN_SECRET, que puedes leer y cambiar desde Variables. Verifícala sin escribir HMAC a mano:

const handle = met.tools.createHandler(process.env.FIRMA_PRECIOS);
app.post('/precios', (req, res) => {
  let args;
  try { args = handle(req.rawBody, req.headers['x-met-signature']); }
  catch { return res.status(400).end(); }
  res.json({ result: buscarPrecio(args.sku) });
});

El cuerpo son los argumentos, planos. Si tu Función declara un parámetro sku, lo que llega es {"sku":"A-1"} — sin sobre y sin un campo arguments que los envuelva. Una versión anterior de esta guía mostraba call.arguments, que nunca existió: si copiaste ese ejemplo, tu handler está recibiendo undefined. Un GET no lleva cuerpo: ahí los argumentos van en la URL.

La firma se calcula sobre el cuerpo crudo. Si tu framework ya lo parseó y lo vuelves a serializar para verificar, la comprobación puede fallar por una diferencia de formato: usa el cuerpo sin tocar (req.rawBody en Express).

Los headers estáticos NO sensibles van en headers. timeout_ms es opcional (default 30s, tope 60s).

B) Flujo (flow_id)

Cuando necesitas lógica multi-paso, ramas o varios nodos, apunta la Función a un Meteor Flow (scopes flows:write / automations:execute). El patrón es create → update con el lienzo → publish:

const flow = await met.flows.create({ name: 'llamar-mi-api' });

await met.flows.update(flow.id, {
  draft_definition: {
    nodes: [{ id: 'call', type: 'http.request', url: 'https://api.tu-empresa.com/precios' }],
    edges: [],
  },
});

await met.flows.publish(flow.id);   // el Met solo invoca la versión publicada

const fn = await met.functions.create({ name: 'consultar_precio', parameters, flow_id: flow.id });

El nodo http.request puede firmar la petición con X-Met-Signature igual que el handler directo. Prueba el flujo antes de publicar con met.flows.run(flow.id, { use_draft: true }).

flows.run() responde apenas arranca la ejecución, sin esperar a que termine. Para leer el resultado no llames a findRun() en un bucle: usa met.flows.waitRun(run_id) (en Python, met.flows.wait_run(run_id)), que pregunta cada vez más espaciado (de 1 a 10 s), espera lo que diga la cabecera Retry-After si recibe un 429 y se rinde a los 5 minutos con MetWaitTimeoutError. Si lo que necesitas es enterarte cuando algo falla, mejor suscríbete al webhook automation.run.failed y no consultes nada; hoy no hay webhook para la ejecución de flujo que termina bien.

const { run_id } = await met.flows.run(flow.id, { use_draft: true });
const { run, nodes } = await met.flows.waitRun(run_id);   // success · failed · cancelled

Vincular al Met

Crear la Función no la activa en ningún Met: hay que vincularla.

await met.functions.link(agentId, fn.id);        // idempotente
await met.functions.listForAgent(agentId);       // → [12, 34] (ids vinculados)
await met.functions.unlink(agentId, fn.id);      // desvincular

Para confirmar que tu Función quedó disponible como tool de un Met, usa met.agents.tools(agentId): devuelve cada herramienta con su source ('function' para las tuyas) y si está disabled. Ver Mets y herramientas.

End-to-end

import Met from '@meteor.ia/sdk';

const met = new Met(process.env.MET_API_KEY, { workspaceId: 130 });

// 1. Secreto por variable (no en crudo)
await met.variables.set('api_token', process.env.API_TOKEN, { encrypted: true });

// 2. Función con handler HTTP
const fn = await met.functions.create({
  name: 'consultar_precio',
  description: 'Consulta el precio vigente de un SKU',
  parameters: [{ name: 'sku', description: 'Código de producto', required: true }],
  http: { url: 'https://api.tu-empresa.com/precios', method: 'POST', auth_workspace_variable: 'api_token' },
});

// 3. Vincular al Met y probar con un run que la use
await met.functions.link(agentId, fn.id);

const run = await met.runs.create({ met: 'Valeria', input: '¿Cuánto vale el SKU AB-12?' });
console.log(run.output);   // el Met llamó a tu endpoint y respondió con el precio

Ver Ejecutar Mets (Runs) para el detalle de runs.create.

Nodos que esperan una respuesta del contacto

Algunos nodos mandan un mensaje y suspenden la corrida hasta que el contacto conteste: whatsapp.send_buttons, whatsapp.send_list, channel.ask_options, flow.question, whatsapp.send_cta_url y las plantillas con botones de respuesta rápida (whatsapp.send_template, whatsapp.send_notification). Mientras esperan, el nodo queda en status: waiting.

Cuando la espera se cierra, el output_artifact del nodo (lo ves en met.flows.findRun(runId), en cada elemento de nodes) trae estos campos:

CampoQué es
resolutionPor qué se cerró la espera (tabla de abajo).
repliedfalse solo si venció la espera; true en cualquier otro caso, incluido no_match.
selection{ id, title } de la opción elegida; en question y reply, { title } con el texto que escribió el contacto. null en no_match y timeout.
_flow_taken_handleLa salida por la que siguió el flujo: el id del botón, out o fallback.
saved_to / saved_valueSolo si el nodo guarda la respuesta en un campo del contacto.

Los valores de resolution:

ValorCuándoSalida
buttonTocó un botón, o eligió una opción de channel.ask_options tocándola o escribiéndola (su número o su texto).La del botón, o out
listEligió una fila de una lista.out
questionRespondió el flow.question.out
replyVolvió a escribir después de un whatsapp.send_cta_url.out
no_matchLlegó un mensaje que no es ninguna de las opciones: texto libre, un botón de otro mensaje, o un flow.question respondido solo con un adjunto.fallback
timeoutVenció el tiempo de espera sin respuesta.fallback

no_match y timeout salen por la misma rama, fallback («Sin respuesta»). Si tu flujo tiene que tratarlos distinto —por ejemplo, insistir cuando el contacto escribió otra cosa y cerrar el caso cuando no contestó—, conecta la rama fallback a un flow.condition que compare @{node:<id del nodo>.resolution} con timeout.