This document demonstrates how to use the Crossplane Function SDK for TypeScript to build your own functions.
npm install @crossplane-org/function-sdk-typescriptA function is a plain function handed the request and a response to fill in:
import {
Resource,
normal,
type ComposeFunction,
} from "@crossplane-org/function-sdk-typescript";
export const compose: ComposeFunction = (req, rsp, logger) => {
logger?.info("Processing function request");
rsp.desired.resources["my-config"] = Resource.fromJSON({
resource: {
apiVersion: "v1",
kind: "ConfigMap",
metadata: { name: "my-config" },
data: { key: "value" },
},
});
normal(rsp, "Function completed successfully");
return rsp;
};The response is already initialized from the request, so there is no need to call
to(), and its desired state is guaranteed to be present — you can write
rsp.desired.resources[name] without a non-null assertion. Return the response you
want sent; returning it is required, so forgetting is a compile error rather than an
empty response at runtime.
Note that when the request already carries desired state — as it does for every
function after the first in a pipeline — rsp.desired is the same object as
req.desired, not a copy.
If you would rather implement the full interface, serve() accepts that too:
import {
FunctionHandler,
RunFunctionRequest,
RunFunctionResponse,
Resource,
to,
normal,
fatal,
getObservedCompositeResource,
getDesiredCompositeResource,
getDesiredComposedResources,
setDesiredComposedResources,
} from "@crossplane-org/function-sdk-typescript";
import type { Logger } from "@crossplane-org/function-sdk-typescript";
export class MyFunction implements FunctionHandler {
async RunFunction(
req: RunFunctionRequest,
logger?: Logger,
): Promise<RunFunctionResponse> {
// Initialize response from request
let rsp = to(req);
try {
// Get observed and desired state
const oxr = getObservedCompositeResource(req);
const dxr = getDesiredCompositeResource(req);
let dcds = getDesiredComposedResources(req);
logger?.info("Processing function request");
// Your function logic here
// Example: Create a Deployment resource
dcds["my-deployment"] = Resource.fromJSON({
resource: {
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: "my-deployment",
namespace: "default",
},
spec: {
replicas: 3,
selector: {
matchLabels: {
app: "my-app",
},
},
template: {
metadata: {
labels: {
app: "my-app",
},
},
spec: {
containers: [
{
name: "my-container",
image: "my-image:latest",
},
],
},
},
},
},
});
// Update response with desired composed resources
rsp = setDesiredComposedResources(rsp, dcds);
normal(rsp, "Function completed successfully");
return rsp;
} catch (error) {
logger?.error({ error }, "Function failed");
fatal(rsp, error instanceof Error ? error.message : String(error));
return rsp;
}
}
}Create a main.ts that hands your function to serve():
#!/usr/bin/env node
import { serve } from "@crossplane-org/function-sdk-typescript";
import { compose } from "./my-function.js";
serve(compose);That is the whole entry point. serve() parses the standard function flags, builds a
logger from --debug, starts the gRPC server, and shuts it down cleanly on SIGINT
and SIGTERM. It accepts either a ComposeFunction or a FunctionHandler.
Every function served this way accepts the same flags:
Usage: main.js [flags]
A Crossplane composition function.
Flags:
--address <value> Address to listen for gRPC connections. Default 0.0.0.0:9443.
-d, --debug Emit debug logs.
--insecure Run without mTLS credentials.
--tls-server-certs-dir <value> Directory holding tls.key, tls.crt and ca.crt. Default /tls/server.
-h, --help Show this help.
serve() takes an options object for the cases where the defaults do not fit:
serve(compose, {
name: "my-function", // Program name shown in --help
argv: ["--insecure"], // Defaults to process.argv.slice(2)
logger: myLogger, // Defaults to a pino logger built from --debug
serverOptions: { insecure: true } // Overrides applied on top of the parsed flags
});# Build your function
npm run build
# Run locally (insecure mode for testing)
node dist/main.js --insecure --debug
# Run with mTLS (production)
node dist/main.js --tls-server-certs-dir /path/to/certsserve() is the recommended entry point, but the pieces it uses are exported, so a
function that needs to own the process — extra flags, a different logger, its own
signal handling — can assemble them directly:
#!/usr/bin/env node
import { pino } from "pino";
import {
FunctionRunner,
newGrpcServer,
parseArgs,
startServer,
type ServerOptions,
} from "@crossplane-org/function-sdk-typescript";
import { MyFunction } from "./my-function.js";
// parseArgs handles the standard flags, so your own parser only has to add to them.
const { help, ...opts } = parseArgs(process.argv.slice(2));
const logger = pino({
level: opts.debug ? "debug" : "info",
formatters: {
level: (label) => ({ severity: label.toUpperCase() }),
},
});
const server = newGrpcServer(new FunctionRunner(new MyFunction(), logger), logger);
startServer(server, opts as ServerOptions, logger);
process.on("SIGTERM", () => {
server.tryShutdown(() => process.exit(0));
});The SDK works well with the kubernetes-models library for type-safe Kubernetes resource creation:
import { Pod } from "kubernetes-models/v1";
import { Resource } from "@crossplane-org/function-sdk-typescript";
const pod = new Pod({
metadata: {
name: "my-pod",
namespace: "default",
},
spec: {
containers: [
{
name: "app",
image: "nginx:latest",
},
],
},
});
pod.validate();
dcds["my-pod"] = Resource.fromJSON({ resource: pod.toJSON() });You can update the status of the composite resource:
import { setDesiredCompositeStatus } from "@crossplane-org/function-sdk-typescript";
rsp = setDesiredCompositeStatus({
rsp,
status: {
ready: true,
message: "All resources created successfully",
},
});Pass data between functions in a pipeline using context:
import { getContextKey, setContextKey } from "@crossplane-org/function-sdk-typescript";
// Read context from previous function
const [resourceId, exists] = getContextKey(req, "resourceId");
if (exists) {
logger?.info({ resourceId }, "Found resource ID from previous function");
}
// Set context for next function
rsp = setContextKey(rsp, "resourceId", "my-resource-123");
rsp = setContextKey(rsp, "status", { created: true, ready: false });Access credentials passed to the function:
import { getCredentials } from "@crossplane-org/function-sdk-typescript";
try {
const creds = getCredentials(req, "aws-credentials");
const accessKey = creds.credentialData?.data["access-key-id"];
const secretKey = creds.credentialData?.data["secret-access-key"];
if (accessKey && secretKey) {
logger?.info("Successfully retrieved AWS credentials");
// Use credentials to interact with AWS
}
} catch (error) {
fatal(rsp, `Failed to get credentials: ${error.message}`);
}Use the result helpers to report errors and warnings:
import { fatal, warning, normal } from "@crossplane-org/function-sdk-typescript";
// Report a fatal error (stops pipeline)
fatal(rsp, "Critical error occurred");
// Report a warning (continues pipeline)
warning(rsp, "Non-critical issue detected");
// Report normal completion
normal(rsp, "Function completed successfully");ComposeFunction- A plain function given the request and a response to fill inComposeResponse- ARunFunctionResponsewhosedesiredstate is guaranteed presentFunctionHandler- Interface to implement for your function logicFunctionRunner- Wraps your handler with error handling and logginggetServer()- Creates a gRPC server with your function
getObservedCompositeResource(req)- Get the observed composite resource (returnsResource | undefined)getDesiredCompositeResource(req)- Get the desired composite resource (returnsResource | undefined)getDesiredComposedResources(req)- Get map of desired composed resources (returns empty object if none exist)getObservedComposedResources(req)- Get map of observed composed resources (returns empty object if none exist)getInput(req)- Get function input configuration (returnsundefinedif not present)getContextKey(req, key)- Get context value from previous function (returns[value, exists]tuple)getRequiredResources(req)- Get required resources mapgetCredentials(req, name)- Get credentials by name (throws error if not found)
to(req, ttl?)- Initialize response from request (optional TTL in seconds, defaults to 60)setDesiredComposedResources(rsp, resources)- Set composed resources (merges with existing)updateDesiredComposedResources(rsp, resources)- Alias forsetDesiredComposedResourcessetDesiredCompositeResource(rsp, resource)- Set the desired composite resourcesetDesiredCompositeStatus({ rsp, status })- Update composite statussetContextKey(rsp, key, value)- Set context for next function in pipelinesetOutput(rsp, output)- Set function output (returned to user)fatal(rsp, message)- Add fatal error result (stops pipeline)warning(rsp, message)- Add warning result (continues pipeline)normal(rsp, message)- Add normal info resultupdate(source, target)- Deep merge resources using ts-deepmerge
serve(fn, opts?)- Run aComposeFunctionorFunctionHandleras a gRPC server: parses flags, builds a logger, starts the server, handles shutdownfromCompose(compose)- Adapt aComposeFunctionto theFunctionHandlerinterfaceparseArgs(argv)- Parse the standard function flags, for functions adding flags of their ownhelpText(name)- The--helptext for the standard flagsDEFAULT_ADDRESS-0.0.0.0:9443DEFAULT_TLS_SERVER_CERTS_DIR-/tls/servernewGrpcServer(runner, logger)- Create gRPC server instancestartServer(server, opts, logger)- Bind and start the server on specified addressgetServerCredentials(opts)- Create server credentials (TLS or insecure mode)
fromObject(obj)- Create a Resource from a plain JavaScript objecttoObject(resource)- Extract plain object from a ResourceasStruct(obj)- Convert object to protobuf Struct formatasObject(struct)- Convert protobuf Struct to plain objectnewDesiredComposed()- Create a new empty DesiredComposed resourcemustStructObject(obj)- Convert object to Struct, throws on errormustStructJSON(json)- Parse JSON string to Struct, throws on error
my-function/
├── src/
│ ├── main.ts # Entry point
│ └── my-function.ts # Your function implementation
├── package.json
├── tsconfig.json
└── dist/ # Build output