Functions and flows
Make your Met call your own backend — a Function with an HTTP handler or a flow, linked to the Met.
An AI Function is ONE tool the Met can call (scopes functions:read / functions:write). When the Met decides to use it, it collects the arguments according to your parameters and fires the handler you defined. That is how you connect your own backend to the conversation.
Parameters: the tool's schema
Each FunctionParameter goes into the JSON schema the LLM sees. Only name is required:
const parameters = [
{ name: 'sku', description: 'Product code', required: true },
{ name: 'currency', description: 'Price currency', enum_values: ['COP', 'USD'] },
];
Available fields: name, description, required, type ('string' | 'number' | 'boolean'), enum_values (closed list; null/[] = free) and save_to (the contact's field_key where the collected value should persist, or null to not store it).
A Function without a handler is inert (not callable). You have two ways to give it one.
A) Direct HTTP handler
The shortest path to hitting your endpoint: pass http. The arguments the Met collects travel to your URL as a JSON body.
const fn = await met.functions.create({
name: 'check_price',
description: 'Looks up the current price of a SKU in our backend',
prompt: 'Use it when the customer asks about a product price.',
parameters,
http: {
url: 'https://api.your-company.com/prices',
method: 'POST',
auth_workspace_variable: 'api_token',
sign_secret_variable: 'price_signature',
timeout_ms: 15000,
},
});
Secrets do NOT go in the raw.
auth_workspace_variable(Bearer token, sent asAuthorization) andsign_secret_variable(signing secret) are referenced by workspace variable name. Create them first withmet.variables.set('api_token', 'sk_live_…', { encrypted: true })— see Workspace variables.
Meteor signs every call with X-Met-Signature (the same HMAC as outbound events), and it is no longer optional: if you don't set sign_secret_variable, Meteor generates a secret for your workspace in the MET_TOOL_SIGN_SECRET variable, which you can read and rotate from Variables. Verify it without writing HMAC by hand:
const handle = met.tools.createHandler(process.env.PRICE_SIGNATURE);
app.post('/prices', (req, res) => {
let args;
try { args = handle(req.rawBody, req.headers['x-met-signature']); }
catch { return res.status(400).end(); }
res.json({ result: lookUpPrice(args.sku) });
});
The body IS the arguments, flat. If your Function declares a
skuparameter, what arrives is{"sku":"A-1"}— no envelope, noargumentsfield wrapping them. An earlier version of this guide showedcall.arguments, which never existed: if you copied that example, your handler has been receivingundefined. AGETcarries no body: there the arguments travel in the URL.
The signature is computed over the raw body. If your framework already parsed it and you re-serialize it to verify, the check can fail on a formatting difference: use the untouched body (req.rawBody in Express).
Static, NON-sensitive headers go in headers. timeout_ms is optional (default 30s, ceiling 60s).
B) Flow (flow_id)
When you need multi-step logic, branches or several nodes, point the Function at a Meteor Flow (scopes flows:write / automations:execute). The pattern is create → update with the canvas → publish:
const flow = await met.flows.create({ name: 'call-my-api' });
await met.flows.update(flow.id, {
draft_definition: {
nodes: [{ id: 'call', type: 'http.request', url: 'https://api.your-company.com/prices' }],
edges: [],
},
});
await met.flows.publish(flow.id); // the Met only invokes the published version
const fn = await met.functions.create({ name: 'check_price', parameters, flow_id: flow.id });
The http.request node can sign the request with X-Met-Signature just like the direct handler. Test the flow before publishing with met.flows.run(flow.id, { use_draft: true }).
flows.run() returns as soon as the run starts, without waiting for it to finish. To read the result, don't call findRun() in a loop: use met.flows.waitRun(run_id) (in Python, met.flows.wait_run(run_id)), which asks less and less often (from 1 to 10 s), waits for whatever the Retry-After header says if it gets a 429, and gives up after 5 minutes with MetWaitTimeoutError. If what you need is to know when something fails, subscribe to the automation.run.failed webhook instead and don't poll at all; there is no webhook yet for a flow run that finishes successfully.
const { run_id } = await met.flows.run(flow.id, { use_draft: true });
const { run, nodes } = await met.flows.waitRun(run_id); // success · failed · cancelled
Link it to the Met
Creating the Function does not activate it on any Met: it has to be linked.
await met.functions.link(agentId, fn.id); // idempotent
await met.functions.listForAgent(agentId); // → [12, 34] (linked ids)
await met.functions.unlink(agentId, fn.id); // unlink
To confirm your Function ended up available as a tool of a Met, use
met.agents.tools(agentId): it returns each tool with itssource('function'for yours) and whether it isdisabled. See Mets and their tools.
End-to-end
import Met from '@meteor.ia/sdk';
const met = new Met(process.env.MET_API_KEY, { workspaceId: 130 });
// 1. Secret by variable (not in the raw)
await met.variables.set('api_token', process.env.API_TOKEN, { encrypted: true });
// 2. Function with an HTTP handler
const fn = await met.functions.create({
name: 'check_price',
description: 'Looks up the current price of a SKU',
parameters: [{ name: 'sku', description: 'Product code', required: true }],
http: { url: 'https://api.your-company.com/prices', method: 'POST', auth_workspace_variable: 'api_token' },
});
// 3. Link it to the Met and try it with a run that uses it
await met.functions.link(agentId, fn.id);
const run = await met.runs.create({ met: 'Valeria', input: 'How much is SKU AB-12?' });
console.log(run.output); // the Met called your endpoint and answered with the price
See Run Mets (Runs) for the details of runs.create.
Nodes that wait for the contact's answer
Some nodes send a message and suspend the run until the contact answers: whatsapp.send_buttons, whatsapp.send_list, channel.ask_options, flow.question, whatsapp.send_cta_url and templates with quick-reply buttons (whatsapp.send_template, whatsapp.send_notification). While waiting, the node sits in status: waiting.
When the wait closes, the node's output_artifact (you see it in met.flows.findRun(runId), on each item of nodes) carries these fields:
| Field | What it is |
|---|---|
resolution | Why the wait closed (table below). |
replied | false only when the wait expired; true in every other case, no_match included. |
selection | { id, title } of the chosen option; for question and reply, { title } with the text the contact wrote. null for no_match and timeout. |
_flow_taken_handle | The output the flow continued through: the button's id, out or fallback. |
saved_to / saved_value | Only when the node saves the answer into a contact field. |
The values of resolution:
| Value | When | Output |
|---|---|---|
button | Tapped a button, or picked a channel.ask_options option by tapping or typing it (its number or its text). | The button's, or out |
list | Picked a row from a list. | out |
question | Answered the flow.question. | out |
reply | Wrote again after a whatsapp.send_cta_url. | out |
no_match | A message arrived that is none of the options: free text, a button from another message, or a flow.question answered with only an attachment. | fallback |
timeout | The wait expired with no answer. | fallback |
no_matchandtimeoutleave through the same branch,fallback("No answer"). If your flow has to treat them differently —say, nudge the contact when they wrote something else and close the case when they never answered—, wire thefallbackbranch to aflow.conditionthat compares@{node:<node id>.resolution}withtimeout.