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, all within a secure and consistent API environment.
By treating logic like a first-class data source, PromptQL ensures your application has a unified surface 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.
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
/**
* Takes an optional name parameter and returns a friendly greeting string
* @param {string} [name] - Optional name to personalize the greeting
* @returns {string} A greeting in the format "hello {name}" or "hello world" if no name provided
* @readonly
*/
export function hello(name?: string) {
return `hello ${name ?? "world"}`;
}
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, DDN will create 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 typing import Annotated
connector = FunctionConnector()
@connector.register_query
def hello(name: str | None = None) -> str:
"""
Takes an optional name parameter and returns a friendly greeting string
"""
return f"hello {name or 'world'}"
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.
package functions
import (
"context"
"fmt"
"hasura-ndc.dev/ndc-go/types"
)
// HelloArguments defines the input parameters
type HelloArguments struct {
Name *string `json:"name"` // Pointer makes it optional
}
// HelloResult defines the return type
type HelloResult struct {
Greeting string `json:"greeting"`
}
// FunctionHello takes an optional name parameter and returns a friendly greeting string
func FunctionHello(ctx context.Context, state *types.State, args *HelloArguments) (*HelloResult, error) {
name := "world"
if args.Name != nil {
name = *args.Name
}
return &HelloResult{
Greeting: fmt.Sprintf("hello %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
(likeProcedureCreateUser
) are treated as mutations (data modifications)
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
Retrieve information
You can add a lambda function which reaches out to external APIs to retrieve data which PromptQL can use.
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.
Take action
You can use lambda connectors to add custom business logic to your application that takes action on behalf of a user.
Step 1. Add custom logic
- TypeScript
- Python
- Go
/*
* Interface for the response when taking action on behalf of a user.
* Contains success status and message.
*/
interface UserActionResponse {
success: boolean;
message: string;
}
/*
* This function simulates taking an action on behalf of a user. It logs the request made by the user
* and returns a response object indicating the success status and a message.
*
* @param {string} request - What the user wants to do
* @returns {UserActionResponse} - The response object containing success status and message
*/
export function takeActionOnBehalfOfUser(request: string): UserActionResponse {
// In a real application, you'd replace this with your custom business logic.
// You could update data in a database or use an API to update another service.
console.log(`Taking action on behalf of user: ${request}`);
return {
success: true,
message: `Successfully took action on user's behalf: ${request}`,
};
}
The absence of the @readonly
tag indicates that this function will modify data. PromptQL will ask for confirmation
before calling it.
from hasura_ndc import start
from hasura_ndc.function_connector import FunctionConnector
from pydantic import BaseModel, Field
from hasura_ndc.errors import UnprocessableContent
from typing import Annotated
connector = FunctionConnector()
class UserActionArguments(BaseModel):
request: Annotated[str, Field(description="What the user wants to do")]
class UserActionResponse(BaseModel):
success: bool
message: str
@connector.register_mutation
def take_action_on_behalf_of_user(args: UserActionArguments) -> UserActionResponse:
# In a real application, you'd replace this with business logic
# You could update data in a database or use an API to update another service.
print("Taking action on behalf of user")
return UserActionResponse(
success=True,
message=f"Successfully took action on user's behalf: {args.request}"
)
if __name__ == "__main__":
start(connector)
The addition of the @connector.register_mutation
decorator indicates that the function will modify data, and PromptQL
will ask for confirmation before calling it.
package functions
import (
"context"
"fmt"
"hasura-ndc.dev/ndc-go/types"
)
// TakeActionArguments represents the input arguments for a user action.
type TakeActionArguments struct {
Request string `json:"request"`
}
// TakeActionResponse represents the response after performing a user action.
type TakeActionResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
// ProcedureTakeActionOnBehalfOfUser simulates taking an action for the user.
func ProcedureTakeActionOnBehalfOfUser(
ctx context.Context,
state *types.State,
args *TakeActionArguments,
) (*TakeActionResponse, error) {
// In a real application, you'd replace this with your custom business logic
// You could update data in a database or use an API to update another service.
fmt.Println("Taking action on behalf of user")
return &TakeActionResponse{
Success: true,
Message: fmt.Sprintf("Successfully took action on user's behalf: %s", args.Request),
}, nil
}
By prefixing the function name with Procedure
, we indicate that the function will modify data, and PromptQL will ask
for confirmation before calling it.
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.