Function Calls
Hugr allows you to execute functions directly in GraphQL queries. Functions can be database stored procedures, SQL expressions, HTTP API calls, or custom computations. All functions are exposed through the function field in queries and function field in mutations.
Defining Functions
Functions are defined in schema using the @function directive on extended Function or MutationFunction types:
extend type Function {
# Database function
calculate_discount(
customer_id: Int!
order_total: Float!
): Float @function(name: "calculate_customer_discount")
# SQL expression
convert_currency(
amount: Float!
from_currency: String!
to_currency: String!
): Float @function(
name: "convert_currency"
sql: "SELECT [amount] * get_exchange_rate([from_currency], [to_currency])"
)
# HTTP API call
get_weather(lat: Float!, lon: Float!): WeatherData @function(
name: "fetch_weather"
sql: """
http_data_source_request_scalar(
'[$catalog]',
'/weather',
'GET',
'{}'::JSON,
{lat: [lat], lon: [lon]}::JSON,
'{}'::JSON,
''
)
"""
json_cast: true
)
}
See Schema Definition - Functions for detailed function definition syntax.
Basic Function Calls
Scalar Functions
Functions that return a single scalar value:
query {
function {
calculate_discount(customer_id: 123, order_total: 500.0)
}
}
Response:
{
"data": {
"function": {
"calculate_discount": 50.0
}
}
}
Object-Returning Functions
Functions that return structured types:
query {
function {
get_customer_stats(customer_id: 123) {
total_orders
total_spent
average_order_value
last_order_date
}
}
}
Response:
{
"data": {
"function": {
"get_customer_stats": {
"total_orders": 15,
"total_spent": 1500.50,
"average_order_value": 100.03,
"last_order_date": "2024-03-15T10:30:00Z"
}
}
}
}
Table Functions
Functions that return multiple rows:
query {
function {
get_recommendations(product_id: 456, limit: 5) {
product_id
product_name
score
reason
}
}
}
Response:
{
"data": {
"function": {
"get_recommendations": [
{
"product_id": 789,
"product_name": "Similar Product A",
"score": 0.95,
"reason": "frequently bought together"
},
{
"product_id": 101,
"product_name": "Similar Product B",
"score": 0.87,
"reason": "same category"
}
]
}
}
}
Functions in Modules
When functions are defined with @module directive, they are nested in the module hierarchy:
extend type Function {
current_weather(lat: Float!, lon: Float!): WeatherData
@function(name: "get_weather")
@module(name: "services.weather")
}
Query the function through its module path:
query {
function {
services {
weather {
current_weather(lat: 40.7128, lon: -74.0060) {
temperature
humidity
conditions
}
}
}
}
}
HTTP API Functions
Call external REST APIs as functions. HTTP API functions must be defined within an HTTP data source configuration.
Defining HTTP Functions
HTTP functions use the special http_data_source_request_scalar function available in HTTP data sources:
# In HTTP data source schema definition
extend type Function {
search_places(
query: String!
location: String!
): [Place] @function(
name: "places_search"
sql: """
http_data_source_request_scalar(
[$catalog],
'/places/search',
'GET',
'{\"Authorization\": \"Bearer token\"}'::JSON,
{q: [query], location: [location]}::JSON,
'{}'::JSON,
''
)
"""
json_cast: true
is_table: true
)
@module(name: "external.places")
}
type Place {
name: String!
address: String
rating: Float
price_level: Int
}
Important: The [$catalog] variable automatically resolves to the current HTTP data source name. HTTP functions can only be defined within HTTP data source configurations where the base URL, authentication, and other HTTP settings are configured.
Calling HTTP Functions
query {
function {
external {
places {
search_places(query: "restaurants", location: "New York") {
name
address
rating
price_level
}
}
}
}
}
HTTP Function Parameters
The http_data_source_request_scalar function signature:
http_data_source_request_scalar(
catalog, -- Data source name (use [$catalog] for current source)
path, -- API endpoint path
method, -- HTTP method: 'GET', 'POST', 'PUT', 'DELETE'
headers, -- Request headers as JSON object
query_params, -- URL query parameters as JSON object
body, -- Request body as JSON object
jq -- Optional jq expression to transform response
)
See HTTP Data Source Configuration for details on configuring HTTP data sources.
Mutation Functions
Define functions that modify data:
extend type MutationFunction {
place_order(
customer_id: Int!
items: JSON!
): OrderResult @function(name: "create_order")
cancel_order(order_id: Int!): Boolean @function(
name: "cancel_order"
sql: "SELECT cancel_order_transaction([order_id])"
)
}
Call mutation functions:
mutation {
function {
place_order(
customer_id: 123
items: "{\"product_id\": 456, \"quantity\": 2}"
) {
order_id
status
total_amount
}
}
}
Parameterized Queries with Variables
Use GraphQL variables for dynamic function calls:
query GetWeather($latitude: Float!, $longitude: Float!) {
function {
services {
weather {
current_weather(lat: $latitude, lon: $longitude) {
temperature
conditions
}
}
}
}
}
Variables:
{
"latitude": 40.7128,
"longitude": -74.0060
}
Combining Multiple Function Calls
Execute multiple functions in a single query:
query {
function {
# Customer statistics
customer_stats: get_customer_stats(customer_id: 123) {
total_orders
total_spent
}
# Product recommendations
recommendations: get_recommendations(
customer_id: 123
limit: 5
) {
product_id
product_name
score
}
# External API call
services {
weather {
current_weather(lat: 40.7, lon: -74.0) {
temperature
}
}
}
}
}
Function Return Types
Functions can return:
1. Scalars
extend type Function {
random_number: Float @function(name: "random")
}
query {
function {
random_number
}
}
2. Custom Types
type Statistics {
mean: Float
median: Float
std_dev: Float
}
extend type Function {
calculate_stats(values: [Float!]!): Statistics
@function(name: "stats")
}
query {
function {
calculate_stats(values: [1.0, 2.0, 3.0, 4.0, 5.0]) {
mean
median
std_dev
}
}
}
3. Arrays
extend type Function {
get_top_products(limit: Int!): [Product]
@function(name: "top_products", is_table: true)
}
query {
function {
get_top_products(limit: 10) {
id
name
sales
}
}
}
Handling NULL Arguments
Control how NULL arguments are handled:
extend type Function {
geocode_address(address: String): Geometry
@function(
name: "geocode"
skip_null_arg: true # Don't pass NULL to SQL function
)
}
When skip_null_arg: true:
- The function will be called even if the argument is NULL
- But the NULL value won't be passed to the SQL function
- Useful for functions that can handle missing optional parameters
Without skip_null_arg, NULL arguments are passed as NULL to the function.
JSON Casting
For functions returning JSON that should be cast to structured types:
extend type Function {
get_config: Configuration
@function(
name: "get_system_config"
json_cast: true # Cast JSON to Configuration type
)
}
Error Handling
Function errors are categorized into two types:
Planning Errors (SQL Generation)
Validation errors caught during query planning, before SQL execution. Include specific error paths and detailed messages:
Invalid function arguments:
query {
function {
calculate_discount(price: "invalid") # Wrong type
}
}
Response:
{
"data": null,
"errors": [
{
"message": "Argument 'price' expects type Float, got String",
"path": ["function", "calculate_discount"],
"extensions": {
"code": "INVALID_ARGUMENT_TYPE"
}
}
]
}
Missing required arguments:
query {
function {
divide(a: 10) # Missing required argument 'b'
}
}
Response:
{
"data": null,
"errors": [
{
"message": "Required argument 'b' not provided",
"path": ["function", "divide"]
}
]
}
SQL Execution Errors
Runtime errors during SQL execution in the database, reported at query level:
query {
function {
divide(a: 10, b: 0) # Division by zero during execution
}
}
Response:
{
"data": null,
"errors": [
{
"message": "division by zero"
}
]
}
Error categories:
- Planning errors - Caught during SQL generation (validation, type checking, field names). Include error paths.
- Execution errors - Caught during SQL execution (database runtime errors). Reported at query level.
Performance Considerations
1. Use Table Functions Efficiently
For functions called multiple times, prefer table functions that return multiple rows:
# Instead of multiple calls
query {
function {
product1: get_price(product_id: 1)
product2: get_price(product_id: 2)
product3: get_price(product_id: 3)
}
}
# Use table function
query {
function {
get_prices(product_ids: [1, 2, 3]) {
product_id
price
}
}
}
2. Cache Function Results
Use caching for expensive function calls:
query {
function {
expensive_calculation(input: 123) @cache(ttl: 300) {
result
}
}
}
3. Limit Result Sets
Always limit table function results:
query {
function {
search_products(query: "laptop", limit: 20) {
id
name
price
}
}
}
Best Practices
- Use meaningful names - Function names should clearly describe their purpose
- Validate inputs - Implement validation in the function, not in the client
- Return structured data - Prefer structured types over JSON when possible
- Document return types - Always define explicit return types in schema
- Handle errors gracefully - Return meaningful error messages
- Consider performance - Optimize database functions and API calls
- Use modules - Organize functions logically using the
@moduledirective
Next Steps
- Learn about Basic Queries for retrieving data by primary keys and unique constraints
- See Function Fields to embed function calls as fields in data objects
- Check Schema Definition - Functions for detailed function configuration