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.
1. Register the chat service
Section titled “1. Register the chat service”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".
2. Write the function
Section titled “2. Write the function”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}/functionsexport 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.
3. Run it from a rule
Section titled “3. Run it from a rule”{ "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.
