Skip to main content

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

  1. Use meaningful names - Function names should clearly describe their purpose
  2. Validate inputs - Implement validation in the function, not in the client
  3. Return structured data - Prefer structured types over JSON when possible
  4. Document return types - Always define explicit return types in schema
  5. Handle errors gracefully - Return meaningful error messages
  6. Consider performance - Optimize database functions and API calls
  7. Use modules - Organize functions logically using the @module directive

Next Steps