Skip to content

Call an LLM and store the result on a record

Summarize a note, classify a ticket, draft a reply: send record data to an OpenAI-compatible chat endpoint and write the answer back to the record. A call_webservice action cannot do this because its response never returns to the record. A function can: it calls the service through a host capability, and its return value is merged into the record.

A function’s Host.invokeService sends no path, so the service’s base_url must be the complete endpoint. Put the API key in auth_config for a bearer provider (OpenAI, Groq, OpenRouter):

{
"name": "Chat LLM",
"service_type": "rest",
"base_url": "https://api.openai.com/v1/chat/completions",
"auth_type": "bearer",
"auth_config": {"token": "<api-key>"},
"headers": {"Content-Type": "application/json"}
}

Allow-list the host (see the outbound-service-call recipe). A key-less local or self-hosted endpoint uses auth_type: "none".

Create a function whose TypeScript handler reads the record, calls the service, and returns the fields to store. Host.invokeService(serviceId, body) returns {status_code, body}; annotate the parameters and the response as any (the guest JSON is a typed union, so property access needs the cast) and the name must be lowercase with hyphens:

POST /api/v1/applications/{application_id}/functions
export function handler(input: any): any {
const notes = (input.record && input.record.notes) || "";
const resp: any = Host.invokeService(input.input.service_config_id, {
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Summarize this CRM note in one sentence: " + notes }],
max_tokens: 200,
temperature: 0
});
const body: any = resp.body || {};
const choice = (body.choices && body.choices[0] && body.choices[0].message) || {};
return { notes_summary: (choice.content || "").trim() };
}

The object returned becomes record_updates and is merged into the record, so add a notes_summary field to the model. Functions have no direct network access; the only egress is Host.invokeService to a configured, allow-listed service. Host.queryRecords and Host.upsertRecord are scoped to the calling actor’s own rows by default; a function explicitly published with runs_as: application retains application-wide access.

{
"type": "run_function",
"params": {"function_id": "<function-uuid>", "service_config_id": "<service-uuid>"}
}

Every param other than function_id is passed to the guest as input, which is why service_config_id arrives at input.input.service_config_id. On record_create / record_update the record now carries the model’s answer.

Note for reasoning models: give a generous max_tokens (or disable thinking) so the token budget is not spent before the answer, or content comes back empty.