Follow the full guide in docs/AddFunctionBody.md.
File Locations
| Component | File |
|---|---|
| Function body definition | onnx/defs/<domain>/defs.cc (inline with schema) |
| FunctionBuilder utilities | onnx/defs/function.h |
| Function tests | tests/cpp/function_get_test.cc, tests/cpp/function_verify_test.cc |
Method 1: Simple String-Based Function Body
ONNX_OPERATOR_SET_SCHEMA(
LessOrEqual, 16,
OpSchema()
// ... inputs, outputs, type constraints ...
.TypeAndShapeInferenceFunction(inferenceFunction)
.FunctionBody(R"ONNX(
{
O1 = Less (A, B)
O2 = Equal (A, B)
C = Or (O1, O2)
}
)ONNX"));
With explicit opset version:
.FunctionBody(R"ONNX(...)ONNX", 18) // Valid from opset 18
Referencing attributes with @attr_name:
.FunctionBody(R"ONNX(
{
Alpha = Constant <value_float: float = @alpha>()
AlphaCast = CastLike (Alpha, X)
...
}
)ONNX")
Method 2: Context-Dependent Function Body
For ops whose decomposition varies based on attributes or optional inputs:
static bool BuildFunctionBodyMyOp(
const FunctionBodyBuildContext& ctx,
const OpSchema& schema,
FunctionProto& functionProto) {
FunctionBuilder builder(functionProto);
// Build graph based on ctx.hasInput(), ctx.getAttribute(), etc.
builder.Add("output = SomeOp (input)");
schema.BuildFunction(functionProto);
return true;
}
// Register:
.SetContextDependentFunctionBodyBuilder(BuildFunctionBodyMyOp)
FunctionBuilder API
FunctionBuilder builder(functionProto);
builder.Add("Y = Relu (X)"); // Add node
builder.Const("alpha", std::vector<float>{0.01f}); // Constant tensor
builder.Const1D("axes", int64_t(1)); // 1-D constant
builder.Add(R"( // Multi-line
X_Sub = Sub (X, X_Max)
X_Exp = Exp (X_Sub)
)");
builder.AddOpset("", 18); // Opset dependency
schema.BuildFunction(functionProto); // Finalize
return true;
Multiple Opset Versions
.SetContextDependentFunctionBodyBuilder(builderForOpset13)
.SetContextDependentFunctionBodyBuilder(builderForOpset18, 18)
ONNX Function Body Syntax
Function body strings use the ONNX text format ("onnxtxt"). See the onnxtxt skill for the full syntax cheat sheet, attribute references (@attr_name), Constant <value = ...> forms, CastLike vs Cast, body-subgraph idioms, and parser tests. Quick reminders specific to function bodies:
- Variable names are local intermediates; input/output names must match the schema's declared names.
- Reference enclosing-op attributes with
@attr_name— and only those declared in.Attr(...)calls. - Use
CastLike(notCast) when the target type depends on another input.
Code Style: Prefer Named Functions
Define context-dependent function body builders as separate named functions rather than inline lambdas within ONNX_OPERATOR_SET_SCHEMA. The macro expansion makes setting breakpoints on inline lambdas unreliable in debuggers.
Simple string-based .FunctionBody(R"ONNX(...)ONNX") definitions don't have this issue.
After Making Changes
python onnx/defs/gen_doc.py
lintrunner -a --output oneline
Common Mistakes
- Forgetting
schema.BuildFunction(functionProto)at end of context-dependent builders - Forgetting to
return truefrom the builder function - Variable names conflicting with input/output names
- Using
Castinstead ofCastLikefor dynamic type matching - Function body not producing all declared outputs
- Using
@attr_namefor an attribute not declared in.Attr()calls