Lambda Connector Basics
Introduction
You can use lambda connectors to give PromptQL abilities to execute custom business logic on behalf of the user. We can add a lamdba connector runtime in Node.js with TypeScript, Python, or Go, and expose functions to PromptQL as tools it can use. This means PromptQL isn't limited to querying data — it can trigger logic, run workflows, or transform inputs into actions & automations, all within a secure and consistent API environment.
By treating logic like a first-class data source, PromptQL ensures your application has a unified interface for interacting with databases, API services, and whatever actions you want your application to be able to take. You define how the system should respond to user queries, apply business rules, or even call third-party APIs.
Initialize a lambda connector
ddn connector init your_name_for_the_connector -i
Choose the lambda connector to correspond with the language you'd like to use for your functions.
- TypeScript
- Python
- Go
When you add the hasura/nodejs
connector, the CLI will generate a Node.js package with a functions.ts
file. This
file is the entrypoint for your connector.
As this is a Node.js project, you can easily add any dependencies you desire by running npm i <package-name>
from this
connector's directory.
When you add the hasura/python
connector, the CLI will generate a Python application with a functions.py
file. This
file is the entrypoint for your connector.
As this is a Python project, you can easily add any dependencies you desire by adding them to the requirements.txt
in
this connector's directory.
When you add the hasura/go
connector, the CLI will generate a Go application with a /functions
directory. The
connector will use this directory — and any *.go
file in it — as the entrypoint for your connector.
As this is a Go project, you can easily add any dependencies you desire by adding them to the go.mod
file and running
go mod tidy
from this connector's directory.
Write a function
There are two types of lambdas you can write, functions and procedures.
- Functions are read-only. Queries without side effects. PromptQL will not ask for confirmation before calling them.
- Procedures can mutate data and have side effects. PromptQL will ask for confirmation before calling them.
Lambda functions work best when you use a non-scalar return type as the added context of the shape of the type helps PromptQL understand how the function can and should be used. In the examples below, you'll see various interfaces, types, and structs used as the return types in these functions.
Examples
The following examples show how to create basic lambda functions in each language. You can replace the contents of the
functions.ts
, functions.py
, or any .go
file in the /functions
directory of the Go connector with the following
examples.
- TypeScript
- Python
- Go
/**
* This interface represents the standard response returned by greeting or other playful functions.
*/
interface HelloResponse {
success: boolean;
message: string;
}
/**
* The hello() function takes in an optional name as a string and returns a HelloResponse object with a success boolean and greeting as the message's value.
* @param {string} name - An optional name to greet.
* @returns {HelloResponse} A HelloResponse object greeting the person.
* @readonly Exposes the function as an NDC function (the function should only query data without making modifications)
*/
export function hello(name?: string): HelloResponse {
return {
success: true,
message: `hello ${name ?? "world"}`,
};
}
/**
* The complimentPerson() function pretends to improve a person's day by giving them a compliment.
* @param {string} name - An optional name of the person to compliment.
* @returns {HelloResponse} A HelloResponse object with a positive message.
*/
export function complimentPerson(name?: string): HelloResponse {
return {
success: true,
message: `You're doing great today, ${name ?? "friend"}!`,
};
}
The JSDoc comments are optional, but the first general comment is highly recommended to help PromptQL understand the function's purpose and parameters and will be added to the function's metadata.
The @readonly
tag indicates that the function does not modify any data, and PromptQL will be able to call this without
asking for confirmation. Under the hood, the connector will expose an NDC function for @readonly
lambdas and an NDC
procedure for functions that are not marked as @readonly
.
from hasura_ndc import start
from hasura_ndc.function_connector import FunctionConnector
from pydantic import BaseModel, Field # You only need this import if you plan to have complex inputs/outputs, which function similar to how frameworks like FastAPI do
from hasura_ndc.errors import UnprocessableContent
from typing import Annotated
from typing import Optional
connector = FunctionConnector()
# A standard response model
class HelloResponse(BaseModel):
success: bool
message: str
# The hello() function takes in an optional name and returns a HelloResponse object.
@connector.register_query
def hello(name: Optional[str] = None) -> HelloResponse:
return HelloResponse(success=True, message=f"Hello {name or 'world'}")
# The compliment_person() function pretends to give a nice compliment.
@connector.register_mutation
def compliment_person(name: Optional[str] = None) -> HelloResponse:
return HelloResponse(success=True, message=f"You're doing great today, {name or 'friend'}!")
if __name__ == "__main__":
start(connector)
The docstring comments are optional, but they're highly recommended to help PromptQL understand the function's purpose and parameters and will be added to the function's metadata.
The register_query
decorator indicates that the function does not modify any data, and PromptQL will be able to call
this without asking for confirmation. To create functions that modify data, use the register_mutation
decorator
instead.
First, let's add a new type:
// Sample Response for hello.go and greet.go
type SampleResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
Then, let's modify our hello.go
:
package functions
import (
"context"
"fmt"
"hasura-ndc.dev/ndc-go/types"
)
// This struct represents the input arguments for the hello function.
type HelloArguments struct {
Name *string `json:"name"`
}
// FunctionHello takes in an optional name as a struct field and returns a HelloResponse object
// with a success boolean and a greeting as the message's value.
// If name is not provided, it defaults to "world".
func FunctionHelloGo(ctx context.Context, state *types.State, arguments *HelloArguments) (*types.SampleResponse, error) {
greeting := "world"
if arguments.Name != nil && *arguments.Name != "" {
greeting = *arguments.Name
}
return &types.SampleResponse{
Success: true,
Message: fmt.Sprintf("hello %s", greeting),
}, nil
}
Finally, let's add a compliment.go
:
package functions
import (
"context"
"fmt"
"hasura-ndc.dev/ndc-go/types"
)
// This struct represents the input arguments for the compliment function.
type ComplimentArguments struct {
Name *string `json:"name"`
}
// ProcedureComplimentPerson pretends to improve a person's day by giving them a compliment.
// It takes in an optional name as a struct field and returns a ComplimentResponse object
// with a success boolean and a positive message.
// If name is not provided, it defaults to "friend".
func ProcedureComplimentPersonGo(ctx context.Context, state *types.State, arguments *ComplimentArguments) (*types.SampleResponse, error) {
name := "friend"
if arguments.Name != nil && *arguments.Name != "" {
name = *arguments.Name
}
return &types.SampleResponse{
Success: true,
Message: fmt.Sprintf("You're doing great today, %s!", name),
}, nil
}
Function names are important as they determine how the function will be exposed in the API:
- Functions starting with
Function
(likeFunctionHello
) are treated as queries (read-only) - Functions starting with
Procedure
(likeProcedureComplimentPerson
) are treated as mutations (data modifications) and will require approval.
The function documentation is highly recommended to help PromptQL understand the function's purpose and parameters and will be added to the function's metadata.
Exposing your lambda functions
Once you've created your lambda functions, you need to expose them to PromptQL by generating metadata for them.
Step 1. Introspect the connector and add the metadata
ddn connector introspect <connector-name>
ddn connector show-resources <connector-name>
You should see the command being AVAILABLE
which means that there's not yet metadata representing it.
ddn commands add <connector-name> <command-name>
If you did not add comments to your function, we highly recommend adding a description
to the command object added
above.
PromptQL's performance is improved by providing more context; if you guide its understanding of what a particular function does and how it should be called, you'll get better results and fewer errors.
Step 2. Create and run a new build and test the function
ddn supergraph build local
ddn run docker-start
ddn console --local
Head over to the PromptQL Playground and ask PromptQL to call your lambda function.
say hello to everyone
Or, try your procedure and get a nice compliment (after approval of course!):
Give Rikin a compliment
Retrieve information
The examples above are great templates for learning how to use functions and procedures. A common use for functions is to reach out to external APIs to retrieve data which PromptQL can use. You can learn more about that below.
Step 1. Call an external API
- TypeScript
- Python
- Go
Open the app/connector/typescript/functions.ts
file.
/**
* Calls httpbin.org API to create a personalized greeting for the given name. Takes an optional name parameter and returns a friendly greeting string.
* @param {string} [name] - Optional name to personalize the greeting
* @returns {Promise<{ greeting?: string }>} A Promise resolving to an object containing the optional greeting message
* @readonly
*/
export async function helloFromHttpBin(name?: string): Promise<{ greeting?: string }> {
const greeting = { greeting: name };
const response = await fetch("https://httpbin.org/post", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ greeting: `Hello ${name}!` }),
});
const data: any = await response.json();
return { greeting: data?.json?.greeting };
}
Open the app/connector/python/functions.py
file.
from hasura_ndc import start
from hasura_ndc.function_connector import FunctionConnector
import requests
connector = FunctionConnector()
@connector.register_query
async def hello_from_http_bin(name: str | None = None) -> dict:
"""
Calls httpbin.org API to create a personalized greeting for the given name.
Takes an optional name parameter and returns a friendly greeting string.
"""
response = requests.post(
"https://httpbin.org/post",
json={"greeting": f"Hello {name}!"}
)
data = response.json()
return {"greeting": data.get("json", {}).get("greeting")}
if __name__ == "__main__":
start(connector)
Open a new file in the functions directory:
package functions
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"hasura-ndc.dev/ndc-go/types"
)
// HelloFromHttpBinArguments defines the input parameters
type HelloFromHttpBinArguments struct {
Name *string `json:"name"` // Optional name parameter
}
// HelloFromHttpBinResponse defines the return type
type HelloFromHttpBinResponse struct {
Greeting *string `json:"greeting"` // Optional greeting response
}
// HTTPBinResponse represents the response from httpbin.org
type HTTPBinResponse struct {
JSON struct {
Greeting string `json:"greeting"`
} `json:"json"`
}
// FunctionHelloFromHttpBin calls httpbin.org API to create a personalized greeting
func FunctionHelloFromHttpBin(ctx context.Context, state *types.State, args *HelloFromHttpBinArguments) (*HelloFromHttpBinResponse, error) {
// Prepare the name to use
name := "world"
if args.Name != nil {
name = *args.Name
}
// Create the request payload
payload, err := json.Marshal(map[string]string{
"greeting": fmt.Sprintf("Hello %s!", name),
})
if err != nil {
return nil, fmt.Errorf("failed to create request payload: %w", err)
}
// Send the POST request to httpbin
resp, err := http.Post("https://httpbin.org/post", "application/json", bytes.NewBuffer(payload))
if err != nil {
return nil, fmt.Errorf("HTTP request failed: %w", err)
}
defer resp.Body.Close()
// Read and parse the response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
var httpBinResp HTTPBinResponse
if err := json.Unmarshal(body, &httpBinResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// Extract the greeting from the response
greeting := httpBinResp.JSON.Greeting
return &HelloFromHttpBinResponse{
Greeting: &greeting,
}, nil
}
Remember to expose your lambda functions to PromptQL by following the steps in Exposing your lambda functions.
Many complex reads and writes to your data sources can be accomplished with custom native queries and mutations. Check out the connector-specific reference docs for generating queries and mutations using the native capabilities of your data source.