Repository: crossplane/crossplane

Issue:
Can't use Composition Function to update XR status
### What happened?
Trying to update a status field in my XR, but it seems to alway be overwritten with conditions. I was trying to replicate a `ToCompositeFieldPath` patch, but it never seems to be written. Updating other fields in the desired state such as labels works fine, but status fields look to be uneditable.


### How can we reproduce it?
I have the following in my definition.yaml
```yaml
piVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xbuckets.catalog.projectx.mavenwave.dev
spec:
  group: catalog.projectx.mavenwave.dev
  names:
    kind: XBucket
    plural: xbuckets
  claimNames:
    kind: BucketClaim
    plural: bucketclaims
  defaultCompositionRef:
    name: gcp-bucket
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
...
          status:
            type: object
            properties:
              bucket:
                type: object
                properties:
                  id:
                    type: string
```

Here's a snippet of my Python code that updates the status field.
```python
try:
  Functionio = read_Functionio()
except yaml.parser.ParserError as err:
  sys.stdout.write("cannot parse FunctionIO: {}\n".format(err))
  sys.exit(1)
...
status = deepcopy(Functionio["observed"]["composite"]["resource"]["status"])
Functionio["desired"]["composite"]["resource"]["status"] = status
Functionio["desired"]["composite"]["resource"]["status"]["bucket"] = {"id": "bar"}
Functionio["desired"]["composite"]["resource"]["metadata"]["labels"] = {"id": "bar"}

write_Functionio(Functionio)
```

The `Functionio["desired"]["composite"]["resource"]["metadata"]["labels"] = {"id": "bar"}` works as expected while the `Functionio["desired"]["composite"]["resource"]["status"]["bucket"] = {"id": "bar"}` does not.

### What environment did it happen in?
Crossplane version: 
v0.11.0, v0.11.1, 1.12.0-rc.0

* Cloud provider or hardware configuration
xpkg.upbound.io/upbound/provider-gcp:v0.28.0
* Kubernetes version (use `kubectl version`)
Client Version: version.Info{Major:"1", Minor:"25", GitVersion:"v1.25.6", GitCommit:"ff2c119726cc1f8926fb0585c74b25921e866a28", GitTreeState:"clean", BuildDate:"2023-01-18T19:22:09Z", GoVersion:"go1.19.5", Compiler:"gc", Platform:"darwin/amd64"}
Kustomize Version: v4.5.7
Server Version: version.Info{Major:"1", Minor:"25", GitVersion:"v1.25.3", GitCommit:"434bfd82814af038ad94d62ebe59b133fcb50506", GitTreeState:"clean", BuildDate:"2022-10-25T19:35:11Z", GoVersion:"go1.19.2", Compiler:"gc", Platform:"linux/amd64"}
* Kubernetes distribution (e.g. Tectonic, GKE, OpenShift)
Kind
* OS (e.g. from /etc/os-release)
MacOS Ventura 13.2
* Kernel (e.g. `uname -a`)
Darwin Kernel Version 22.3.0

Discussion:
@negz: Thanks for the bug report. I would certainly expect this to work - it's not immediately obvious to me why it's not.@varunpalekar: Also happening to us.
Child status not seen in parent one.@jan-di: Hi guys, we are hitting this limitation in our function. I debugged this and have an idea why this does not work.

When an XRD creates the respective CRD for the XR, it marks `.status` as a subresource. This aso prevents normal edits via `kubectl apply` and similar commands. I think this may also prevent crossplane from setting the status via a function, as it does a normal apply without considering subresources.

https://github.com/kubernetes/kubectl/issues/564@negz: I'm pretty sure I know why this is happening now, and intend to fix it in https://github.com/crossplane/crossplane/pull/4500.

Edit: I took a pass, but it's harder than it first seemed. 😞 I won't fix it in that PR, but we will need to before this feature gets to beta. We'll address it in a follow-up PR. (CC @ezgidemirel @phisco FYI).@negz: Adding this to the v1.14 milestone because I think it's a blocker for https://github.com/crossplane/crossplane/issues/4571 to work properly.@KevinWang15: Hi, is it currently supported to update the `status` subresource (add custom status fields to the XR) in the composition?@bobh66: Yes - you can patch into the status using `function-patch-and-transform` and `function-go-templating` has a specific method for populating composite status fields (see the examples in the repo)@KevinWang15: Thank you for your prompt response! I will take a look

Solution patch:
diff --git a/apis/apiextensions/fn/io/v1alpha1/functionio_types.go b/apis/apiextensions/fn/io/v1alpha1/functionio_types.go
deleted file mode 100644
index e0026e936..000000000
--- a/apis/apiextensions/fn/io/v1alpha1/functionio_types.go
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
-Copyright 2022 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v1alpha1
-
-import (
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-
-	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
-)
-
-// +kubebuilder:object:root=true
-// +kubebuilder:storageversion
-
-// A FunctionIO represents the I/O of an Composition Function.
-type FunctionIO struct {
-	metav1.TypeMeta `json:",inline"`
-
-	// Config is an opaque Kubernetes object containing optional function
-	// configuration.
-	// +optional
-	// +kubebuilder:validation:EmbeddedResource
-	// +kubebuilder:pruning:PreserveUnknownFields
-	Config *runtime.RawExtension `json:"config,omitempty"`
-
-	// Observed state prior to the invocation of a function pipeline. Functions
-	// must not mutate this state - any attempts to do so will be ignored. State
-	// passed to each function is fresh as of the time the function pipeline was
-	// invoked, not as of the time each function was invoked.
-	Observed Observed `json:"observed"`
-
-	// Desired state according to a function pipeline. The state passed to a
-	// particular function may have been mutated by previous functions in the
-	// pipeline. Functions may mutate any part of the desired state they are
-	// concerned with, and must pass through any part of the desired state that
-	// they are not concerned with.
-	Desired Desired `json:"desired"`
-
-	// Results is an optional list that can be used by function to emit results
-	// for observability and debugging purposes. Functions may mutate any
-	// results that they are concerned with, and must pass through any results
-	// that that are not concerned with.
-	// +optional
-	Results []Result `json:"results,omitempty"`
-}
-
-// Observed state at the beginning of a function pipeline invocation.
-type Observed struct {
-	// Composite reflects the observed state of the XR this function reconciles.
-	Composite ObservedComposite `json:"composite"`
-
-	// Resources reflect the observed state of any extant composed resources
-	// this function reconciles. Only composed resources that currently exist in
-	// the API server (i.e. have been created and not yet deleted) are included.
-	// +optional
-	Resources []ObservedResource `json:"resources,omitempty"`
-}
-
-// An ObservedComposite resource.
-type ObservedComposite struct {
-	// Resource reflects the observed XR.
-	// +kubebuilder:validation:EmbeddedResource
-	// +kubebuilder:pruning:PreserveUnknownFields
-	Resource runtime.RawExtension `json:"resource"`
-
-	// ConnectionDetails reflects the observed connection details of the XR.
-	// +optional
-	ConnectionDetails []ExplicitConnectionDetail `json:"connectionDetails,omitempty"`
-}
-
-// An ObservedResource represents an observed composed resource.
-type ObservedResource struct {
-	// Name of the observed resource. Must be unique within the array of
-	// observed resources. Corresponds to the name entry in a Composition's
-	// resources array, and the name entry in the desired resources array.
-	Name string `json:"name"`
-
-	// Resource reflects the observed composed resource.
-	// +kubebuilder:validation:EmbeddedResource
-	// +kubebuilder:pruning:PreserveUnknownFields
-	Resource runtime.RawExtension `json:"resource"`
-
-	// ConnectionDetails reflects the observed connection details of the
-	// composed resource.
-	ConnectionDetails []ExplicitConnectionDetail `json:"connectionDetails"`
-}
-
-// Desired state of a function pipeline invocation.
-type Desired struct {
-	// Composite reflects the desired state of the XR this function reconciles.
-	Composite DesiredComposite `json:"composite"`
-
-	// Resources reflect the desired state of composed resources, including
-	// those that do not yet exist.
-	// +optional
-	Resources []DesiredResource `json:"resources,omitempty"`
-}
-
-// A DesiredComposite resource.
-type DesiredComposite struct {
-	// Resource reflects the desired XR. Functions may update the metadata,
-	// spec, and status of an XR.
-	// +kubebuilder:validation:EmbeddedResource
-	// +kubebuilder:pruning:PreserveUnknownFields
-	Resource runtime.RawExtension `json:"resource"`
-
-	// ConnectionDetails reflects the desired connection details of the XR.
-	// +optional
-	ConnectionDetails []ExplicitConnectionDetail `json:"connectionDetails"`
-}
-
-// A DesiredResource represents a desired composed resource.
-type DesiredResource struct {
-	// Name of the desired resource. Must be unique within the array of
-	// desired resources. Corresponds to the name entry in a Composition's
-	// resources array, and the name entry in the observed resources array.
-	Name string `json:"name"`
-
-	// Resource reflects the desired composed resource. Functions may update the
-	// metadata and spec of a composed resource. Updates to status will be
-	// discarded.
-	// +kubebuilder:validation:EmbeddedResource
-	// +kubebuilder:pruning:PreserveUnknownFields
-	Resource runtime.RawExtension `json:"resource"`
-
-	// ConnectionDetails reflects _XR_ connection details that should be derived
-	// from this composed resource.
-	// +optional
-	ConnectionDetails []DerivedConnectionDetail `json:"connectionDetails,omitempty"`
-
-	// ReadinessChecks configures how this composed resource will be determined
-	// to be ready.
-	// +optional
-	ReadinessChecks []DesiredReadinessCheck `json:"readinessChecks,omitempty"`
-}
-
-// An ExplicitConnectionDetail is a simple map of name (key) to value.
-type ExplicitConnectionDetail struct {
-	// Name of the connection detail.
-	Name string `json:"name"`
-
-	// Value of the connection detail.
-	Value string `json:"value"`
-}
-
-// A ConnectionDetailType is a type of connection detail.
-type ConnectionDetailType string
-
-// ConnectionDetailType types.
-const (
-	ConnectionDetailTypeUnknown                 ConnectionDetailType = "Unknown"
-	ConnectionDetailTypeFromConnectionSecretKey ConnectionDetailType = "FromConnectionSecretKey"
-	ConnectionDetailTypeFromFieldPath           ConnectionDetailType = "FromFieldPath"
-	ConnectionDetailTypeFromValue               ConnectionDetailType = "FromValue"
-)
-
-// A DerivedConnectionDetail specifies how to derive an XR connection detail
-// from a composed resource.
-type DerivedConnectionDetail struct {
-	// Name of the connection detail that will be propagated to the
-	// connection secret of the XR. Can be omitted for FromConnectionDetailKey,
-	// in which case it will default to that key.
-	// +optional
-	Name *string `json:"name,omitempty"`
-
-	// Type sets the connection detail fetching behaviour to be used. Each
-	// connection detail type may require its own fields to be set on the
-	// ConnectionDetail object.
-	// +kubebuilder:validation:Enum=FromConnectionDetailKey;FromFieldPath;FromValue
-	Type ConnectionDetailType `json:"type"`
-
-	// FromConnectionDetailKey sets an XR connection detail to the value of the
-	// supplied connection detail of the composed resource.
-	// +optional
-	FromConnectionSecretKey *string `json:"fromConnectionSecretKey,omitempty"`
-
-	// FromFieldPath sets an XR connection detail to the value at the supplied
-	// fieldpath within the composed resource.
-	// +optional
-	FromFieldPath *string `json:"fromFieldPath,omitempty"`
-
-	// Value that will be propagated to the connection detail of the XR.
-	// +optional
-	Value *string `json:"value,omitempty"`
-}
-
-// ReadinessCheckType is used for readiness check types.
-type ReadinessCheckType string
-
-// The possible values for readiness check type.
-const (
-	ReadinessCheckTypeNonEmpty       ReadinessCheckType = "NonEmpty"
-	ReadinessCheckTypeMatchString    ReadinessCheckType = "MatchString"
-	ReadinessCheckTypeMatchInteger   ReadinessCheckType = "MatchInteger"
-	ReadinessCheckTypeMatchTrue      ReadinessCheckType = "MatchTrue"
-	ReadinessCheckTypeMatchFalse     ReadinessCheckType = "MatchFalse"
-	ReadinessCheckTypeMatchCondition ReadinessCheckType = "MatchCondition"
-	ReadinessCheckTypeNone           ReadinessCheckType = "None"
-)
-
-// A DesiredReadinessCheck is used to indicate how to tell whether a resource is
-// ready for consumption
-type DesiredReadinessCheck struct {
-	// Type indicates the type of probe you'd like to use.
-	// +kubebuilder:validation:Enum="MatchString";"MatchInteger";"NonEmpty";"MatchTrue";"MatchFalse";"MatchCondition";"None"
-	Type ReadinessCheckType `json:"type"`
-
-	// FieldPath shows the path of the field whose value will be used.
-	// +optional
-	FieldPath *string `json:"fieldPath,omitempty"`
-
-	// MatchString is the value you'd like to match if you're using
-	// "MatchString" type.
-	// +optional
-	MatchString *string `json:"matchString,omitempty"`
-
-	// MatchInt is the value you'd like to match if you're using "MatchInt"
-	// type.
-	// +optional
-	MatchInteger *int64 `json:"matchInteger,omitempty"`
-
-	// MatchCondition specifies the condition you'd like to match if you're using "MatchCondition" type.
-	// +optional
-	MatchCondition *MatchConditionReadinessCheck `json:"matchCondition,omitempty"`
-}
-
-// MatchConditionReadinessCheck is used to indicate how to tell whether a resource is ready
-// for consumption
-type MatchConditionReadinessCheck struct {
-	// Type indicates the type of condition you'd like to use.
-	// +kubebuilder:default="Ready"
-	Type xpv1.ConditionType `json:"type"`
-
-	// Status is the status of the condition you'd like to match.
-	// +kubebuilder:default="True"
-	Status corev1.ConditionStatus `json:"status"`
-}
-
-// Result is an optional list that can be used by function to emit results for
-// observability and debugging purposes.
-type Result struct {
-	// Severity is the severity of a result.
-	//
-	// Fatal results are fatal; subsequent Composition Functions may run, but
-	// the Composition Function pipeline run will be considered a failure and
-	// the first error will be returned.
-	//
-	// Warning results are non-fatal; the entire Composition will run to
-	// completion but warning events and debug logs associated with the
-	// composite resource will be emitted.
-	//
-	// Normal results are emitted as normal events and debug logs associated
-	// with the composite resource.
-	// +kubebuilder:validation:Enum=Fatal;Warning;Normal
-	Severity Severity `json:"severity"`
-
-	// Message is a human readable message.
-	Message string `json:"message"`
-}
-
-// Severity is the severity of a result.
-type Severity string
-
-// Result severities.
-const (
-	SeverityFatal   Severity = "Fatal"
-	SeverityWarning Severity = "Warning"
-	SeverityNormal  Severity = "Normal"
-)
diff --git a/apis/apiextensions/fn/io/v1alpha1/zz_generated.deepcopy.go b/apis/apiextensions/fn/io/v1alpha1/zz_generated.deepcopy.go
deleted file mode 100644
index c5747b26c..000000000
--- a/apis/apiextensions/fn/io/v1alpha1/zz_generated.deepcopy.go
+++ /dev/null
@@ -1,315 +0,0 @@
-//go:build !ignore_autogenerated
-
-/*
-Copyright 2021 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// Code generated by controller-gen. DO NOT EDIT.
-
-package v1alpha1
-
-import (
-	"k8s.io/apimachinery/pkg/runtime"
-)
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *DerivedConnectionDetail) DeepCopyInto(out *DerivedConnectionDetail) {
-	*out = *in
-	if in.Name != nil {
-		in, out := &in.Name, &out.Name
-		*out = new(string)
-		**out = **in
-	}
-	if in.FromConnectionSecretKey != nil {
-		in, out := &in.FromConnectionSecretKey, &out.FromConnectionSecretKey
-		*out = new(string)
-		**out = **in
-	}
-	if in.FromFieldPath != nil {
-		in, out := &in.FromFieldPath, &out.FromFieldPath
-		*out = new(string)
-		**out = **in
-	}
-	if in.Value != nil {
-		in, out := &in.Value, &out.Value
-		*out = new(string)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DerivedConnectionDetail.
-func (in *DerivedConnectionDetail) DeepCopy() *DerivedConnectionDetail {
-	if in == nil {
-		return nil
-	}
-	out := new(DerivedConnectionDetail)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Desired) DeepCopyInto(out *Desired) {
-	*out = *in
-	in.Composite.DeepCopyInto(&out.Composite)
-	if in.Resources != nil {
-		in, out := &in.Resources, &out.Resources
-		*out = make([]DesiredResource, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Desired.
-func (in *Desired) DeepCopy() *Desired {
-	if in == nil {
-		return nil
-	}
-	out := new(Desired)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *DesiredComposite) DeepCopyInto(out *DesiredComposite) {
-	*out = *in
-	in.Resource.DeepCopyInto(&out.Resource)
-	if in.ConnectionDetails != nil {
-		in, out := &in.ConnectionDetails, &out.ConnectionDetails
-		*out = make([]ExplicitConnectionDetail, len(*in))
-		copy(*out, *in)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesiredComposite.
-func (in *DesiredComposite) DeepCopy() *DesiredComposite {
-	if in == nil {
-		return nil
-	}
-	out := new(DesiredComposite)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *DesiredReadinessCheck) DeepCopyInto(out *DesiredReadinessCheck) {
-	*out = *in
-	if in.FieldPath != nil {
-		in, out := &in.FieldPath, &out.FieldPath
-		*out = new(string)
-		**out = **in
-	}
-	if in.MatchString != nil {
-		in, out := &in.MatchString, &out.MatchString
-		*out = new(string)
-		**out = **in
-	}
-	if in.MatchInteger != nil {
-		in, out := &in.MatchInteger, &out.MatchInteger
-		*out = new(int64)
-		**out = **in
-	}
-	if in.MatchCondition != nil {
-		in, out := &in.MatchCondition, &out.MatchCondition
-		*out = new(MatchConditionReadinessCheck)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesiredReadinessCheck.
-func (in *DesiredReadinessCheck) DeepCopy() *DesiredReadinessCheck {
-	if in == nil {
-		return nil
-	}
-	out := new(DesiredReadinessCheck)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *DesiredResource) DeepCopyInto(out *DesiredResource) {
-	*out = *in
-	in.Resource.DeepCopyInto(&out.Resource)
-	if in.ConnectionDetails != nil {
-		in, out := &in.ConnectionDetails, &out.ConnectionDetails
-		*out = make([]DerivedConnectionDetail, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
-	if in.ReadinessChecks != nil {
-		in, out := &in.ReadinessChecks, &out.ReadinessChecks
-		*out = make([]DesiredReadinessCheck, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DesiredResource.
-func (in *DesiredResource) DeepCopy() *DesiredResource {
-	if in == nil {
-		return nil
-	}
-	out := new(DesiredResource)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ExplicitConnectionDetail) DeepCopyInto(out *ExplicitConnectionDetail) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExplicitConnectionDetail.
-func (in *ExplicitConnectionDetail) DeepCopy() *ExplicitConnectionDetail {
-	if in == nil {
-		return nil
-	}
-	out := new(ExplicitConnectionDetail)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionIO) DeepCopyInto(out *FunctionIO) {
-	*out = *in
-	out.TypeMeta = in.TypeMeta
-	if in.Config != nil {
-		in, out := &in.Config, &out.Config
-		*out = new(runtime.RawExtension)
-		(*in).DeepCopyInto(*out)
-	}
-	in.Observed.DeepCopyInto(&out.Observed)
-	in.Desired.DeepCopyInto(&out.Desired)
-	if in.Results != nil {
-		in, out := &in.Results, &out.Results
-		*out = make([]Result, len(*in))
-		copy(*out, *in)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionIO.
-func (in *FunctionIO) DeepCopy() *FunctionIO {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionIO)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *FunctionIO) DeepCopyObject() runtime.Object {
-	if c := in.DeepCopy(); c != nil {
-		return c
-	}
-	return nil
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *MatchConditionReadinessCheck) DeepCopyInto(out *MatchConditionReadinessCheck) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MatchConditionReadinessCheck.
-func (in *MatchConditionReadinessCheck) DeepCopy() *MatchConditionReadinessCheck {
-	if in == nil {
-		return nil
-	}
-	out := new(MatchConditionReadinessCheck)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Observed) DeepCopyInto(out *Observed) {
-	*out = *in
-	in.Composite.DeepCopyInto(&out.Composite)
-	if in.Resources != nil {
-		in, out := &in.Resources, &out.Resources
-		*out = make([]ObservedResource, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Observed.
-func (in *Observed) DeepCopy() *Observed {
-	if in == nil {
-		return nil
-	}
-	out := new(Observed)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ObservedComposite) DeepCopyInto(out *ObservedComposite) {
-	*out = *in
-	in.Resource.DeepCopyInto(&out.Resource)
-	if in.ConnectionDetails != nil {
-		in, out := &in.ConnectionDetails, &out.ConnectionDetails
-		*out = make([]ExplicitConnectionDetail, len(*in))
-		copy(*out, *in)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservedComposite.
-func (in *ObservedComposite) DeepCopy() *ObservedComposite {
-	if in == nil {
-		return nil
-	}
-	out := new(ObservedComposite)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ObservedResource) DeepCopyInto(out *ObservedResource) {
-	*out = *in
-	in.Resource.DeepCopyInto(&out.Resource)
-	if in.ConnectionDetails != nil {
-		in, out := &in.ConnectionDetails, &out.ConnectionDetails
-		*out = make([]ExplicitConnectionDetail, len(*in))
-		copy(*out, *in)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservedResource.
-func (in *ObservedResource) DeepCopy() *ObservedResource {
-	if in == nil {
-		return nil
-	}
-	out := new(ObservedResource)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Result) DeepCopyInto(out *Result) {
-	*out = *in
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Result.
-func (in *Result) DeepCopy() *Result {
-	if in == nil {
-		return nil
-	}
-	out := new(Result)
-	in.DeepCopyInto(out)
-	return out
-}
diff --git a/apis/apiextensions/fn/proto/v1alpha1/run_function.pb.go b/apis/apiextensions/fn/proto/v1alpha1/run_function.pb.go
deleted file mode 100644
index 8929c75e5..000000000
--- a/apis/apiextensions/fn/proto/v1alpha1/run_function.pb.go
+++ /dev/null
@@ -1,922 +0,0 @@
-//
-//Copyright 2022 The Crossplane Authors.
-//
-//Licensed under the Apache License, Version 2.0 (the "License");
-//you may not use this file except in compliance with the License.
-//You may obtain a copy of the License at
-//
-//http://www.apache.org/licenses/LICENSE-2.0
-//
-//Unless required by applicable law or agreed to in writing, software
-//distributed under the License is distributed on an "AS IS" BASIS,
-//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-//See the License for the specific language governing permissions and
-//limitations under the License.
-
-// TODO(negz): Add a make target such that `make lint` runs `buf lint`.
-
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// 	protoc-gen-go v1.31.0
-// 	protoc        (unknown)
-// source: apiextensions/fn/proto/v1alpha1/run_function.proto
-
-package v1alpha1
-
-import (
-	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
-	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
-	durationpb "google.golang.org/protobuf/types/known/durationpb"
-	reflect "reflect"
-	sync "sync"
-)
-
-const (
-	// Verify that this generated code is sufficiently up-to-date.
-	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
-	// Verify that runtime/protoimpl is sufficiently up-to-date.
-	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
-)
-
-// ImagePullPolicy specifies when a Composition Function container should be
-// pulled from a remote OCI registry.
-type ImagePullPolicy int32
-
-const (
-	ImagePullPolicy_IMAGE_PULL_POLICY_UNSPECIFIED    ImagePullPolicy = 0
-	ImagePullPolicy_IMAGE_PULL_POLICY_IF_NOT_PRESENT ImagePullPolicy = 1
-	ImagePullPolicy_IMAGE_PULL_POLICY_ALWAYS         ImagePullPolicy = 2
-	ImagePullPolicy_IMAGE_PULL_POLICY_NEVER          ImagePullPolicy = 3
-)
-
-// Enum value maps for ImagePullPolicy.
-var (
-	ImagePullPolicy_name = map[int32]string{
-		0: "IMAGE_PULL_POLICY_UNSPECIFIED",
-		1: "IMAGE_PULL_POLICY_IF_NOT_PRESENT",
-		2: "IMAGE_PULL_POLICY_ALWAYS",
-		3: "IMAGE_PULL_POLICY_NEVER",
-	}
-	ImagePullPolicy_value = map[string]int32{
-		"IMAGE_PULL_POLICY_UNSPECIFIED":    0,
-		"IMAGE_PULL_POLICY_IF_NOT_PRESENT": 1,
-		"IMAGE_PULL_POLICY_ALWAYS":         2,
-		"IMAGE_PULL_POLICY_NEVER":          3,
-	}
-)
-
-func (x ImagePullPolicy) Enum() *ImagePullPolicy {
-	p := new(ImagePullPolicy)
-	*p = x
-	return p
-}
-
-func (x ImagePullPolicy) String() string {
-	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (ImagePullPolicy) Descriptor() protoreflect.EnumDescriptor {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_enumTypes[0].Descriptor()
-}
-
-func (ImagePullPolicy) Type() protoreflect.EnumType {
-	return &file_apiextensions_fn_proto_v1alpha1_run_function_proto_enumTypes[0]
-}
-
-func (x ImagePullPolicy) Number() protoreflect.EnumNumber {
-	return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use ImagePullPolicy.Descriptor instead.
-func (ImagePullPolicy) EnumDescriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{0}
-}
-
-// NetworkPolicy configures whether a container is isolated from the network.
-type NetworkPolicy int32
-
-const (
-	NetworkPolicy_NETWORK_POLICY_UNSPECIFIED NetworkPolicy = 0
-	// Run the container without network access. The default.
-	NetworkPolicy_NETWORK_POLICY_ISOLATED NetworkPolicy = 1
-	// Allow the container to access the same network as the function runner.
-	NetworkPolicy_NETWORK_POLICY_RUNNER NetworkPolicy = 2
-)
-
-// Enum value maps for NetworkPolicy.
-var (
-	NetworkPolicy_name = map[int32]string{
-		0: "NETWORK_POLICY_UNSPECIFIED",
-		1: "NETWORK_POLICY_ISOLATED",
-		2: "NETWORK_POLICY_RUNNER",
-	}
-	NetworkPolicy_value = map[string]int32{
-		"NETWORK_POLICY_UNSPECIFIED": 0,
-		"NETWORK_POLICY_ISOLATED":    1,
-		"NETWORK_POLICY_RUNNER":      2,
-	}
-)
-
-func (x NetworkPolicy) Enum() *NetworkPolicy {
-	p := new(NetworkPolicy)
-	*p = x
-	return p
-}
-
-func (x NetworkPolicy) String() string {
-	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (NetworkPolicy) Descriptor() protoreflect.EnumDescriptor {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_enumTypes[1].Descriptor()
-}
-
-func (NetworkPolicy) Type() protoreflect.EnumType {
-	return &file_apiextensions_fn_proto_v1alpha1_run_function_proto_enumTypes[1]
-}
-
-func (x NetworkPolicy) Number() protoreflect.EnumNumber {
-	return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use NetworkPolicy.Descriptor instead.
-func (NetworkPolicy) EnumDescriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{1}
-}
-
-// ImagePullAuth configures authentication to a remote OCI registry.
-// It corresponds to go-containerregistry's AuthConfig type.
-// https://pkg.go.dev/github.com/google/go-containerregistry@v0.11.0/pkg/authn#AuthConfig
-type ImagePullAuth struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	Username      string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
-	Password      string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
-	Auth          string `protobuf:"bytes,3,opt,name=auth,proto3" json:"auth,omitempty"`
-	IdentityToken string `protobuf:"bytes,4,opt,name=identity_token,json=identityToken,proto3" json:"identity_token,omitempty"`
-	RegistryToken string `protobuf:"bytes,5,opt,name=registry_token,json=registryToken,proto3" json:"registry_token,omitempty"`
-}
-
-func (x *ImagePullAuth) Reset() {
-	*x = ImagePullAuth{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[0]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *ImagePullAuth) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ImagePullAuth) ProtoMessage() {}
-
-func (x *ImagePullAuth) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[0]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use ImagePullAuth.ProtoReflect.Descriptor instead.
-func (*ImagePullAuth) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{0}
-}
-
-func (x *ImagePullAuth) GetUsername() string {
-	if x != nil {
-		return x.Username
-	}
-	return ""
-}
-
-func (x *ImagePullAuth) GetPassword() string {
-	if x != nil {
-		return x.Password
-	}
-	return ""
-}
-
-func (x *ImagePullAuth) GetAuth() string {
-	if x != nil {
-		return x.Auth
-	}
-	return ""
-}
-
-func (x *ImagePullAuth) GetIdentityToken() string {
-	if x != nil {
-		return x.IdentityToken
-	}
-	return ""
-}
-
-func (x *ImagePullAuth) GetRegistryToken() string {
-	if x != nil {
-		return x.RegistryToken
-	}
-	return ""
-}
-
-// ImagePullConfig configures how a Composition Function container should be
-// pulled from a remote OCI registry.
-type ImagePullConfig struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	PullPolicy ImagePullPolicy `protobuf:"varint,1,opt,name=pull_policy,json=pullPolicy,proto3,enum=apiextensions.fn.proto.v1alpha1.ImagePullPolicy" json:"pull_policy,omitempty"`
-	Auth       *ImagePullAuth  `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"`
-}
-
-func (x *ImagePullConfig) Reset() {
-	*x = ImagePullConfig{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[1]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *ImagePullConfig) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ImagePullConfig) ProtoMessage() {}
-
-func (x *ImagePullConfig) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[1]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use ImagePullConfig.ProtoReflect.Descriptor instead.
-func (*ImagePullConfig) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *ImagePullConfig) GetPullPolicy() ImagePullPolicy {
-	if x != nil {
-		return x.PullPolicy
-	}
-	return ImagePullPolicy_IMAGE_PULL_POLICY_UNSPECIFIED
-}
-
-func (x *ImagePullConfig) GetAuth() *ImagePullAuth {
-	if x != nil {
-		return x.Auth
-	}
-	return nil
-}
-
-// NetworkConfig configures whether and how a Composition Function container may
-// access the network.
-type NetworkConfig struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	// Whether or not the container can access the network.
-	Policy NetworkPolicy `protobuf:"varint,1,opt,name=policy,proto3,enum=apiextensions.fn.proto.v1alpha1.NetworkPolicy" json:"policy,omitempty"`
-}
-
-func (x *NetworkConfig) Reset() {
-	*x = NetworkConfig{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[2]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *NetworkConfig) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*NetworkConfig) ProtoMessage() {}
-
-func (x *NetworkConfig) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[2]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use NetworkConfig.ProtoReflect.Descriptor instead.
-func (*NetworkConfig) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{2}
-}
-
-func (x *NetworkConfig) GetPolicy() NetworkPolicy {
-	if x != nil {
-		return x.Policy
-	}
-	return NetworkPolicy_NETWORK_POLICY_UNSPECIFIED
-}
-
-// Resources configures what compute resources should be available to a
-// Composition Function container.
-type ResourceConfig struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	Limits *ResourceLimits `protobuf:"bytes,1,opt,name=limits,proto3" json:"limits,omitempty"`
-}
-
-func (x *ResourceConfig) Reset() {
-	*x = ResourceConfig{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[3]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *ResourceConfig) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ResourceConfig) ProtoMessage() {}
-
-func (x *ResourceConfig) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[3]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use ResourceConfig.ProtoReflect.Descriptor instead.
-func (*ResourceConfig) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{3}
-}
-
-func (x *ResourceConfig) GetLimits() *ResourceLimits {
-	if x != nil {
-		return x.Limits
-	}
-	return nil
-}
-
-// ResourceLimits configures the maximum compute resources that will be
-// available to a Composition Function container.
-type ResourceLimits struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	// CPU, in cores. (500m = .5 cores)
-	// Specified in Kubernetes-style resource.Quantity form.
-	Memory string `protobuf:"bytes,1,opt,name=memory,proto3" json:"memory,omitempty"`
-	// Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
-	// Specified in Kubernetes-style resource.Quantity form.
-	Cpu string `protobuf:"bytes,2,opt,name=cpu,proto3" json:"cpu,omitempty"`
-}
-
-func (x *ResourceLimits) Reset() {
-	*x = ResourceLimits{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[4]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *ResourceLimits) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ResourceLimits) ProtoMessage() {}
-
-func (x *ResourceLimits) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[4]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use ResourceLimits.ProtoReflect.Descriptor instead.
-func (*ResourceLimits) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{4}
-}
-
-func (x *ResourceLimits) GetMemory() string {
-	if x != nil {
-		return x.Memory
-	}
-	return ""
-}
-
-func (x *ResourceLimits) GetCpu() string {
-	if x != nil {
-		return x.Cpu
-	}
-	return ""
-}
-
-// RunFunctionConfig configures how a Composition Function container is run.
-type RunFunctionConfig struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	// Resources available to the container.
-	Resources *ResourceConfig `protobuf:"bytes,1,opt,name=resources,proto3" json:"resources,omitempty"`
-	// Network configuration for the container.
-	Network *NetworkConfig `protobuf:"bytes,2,opt,name=network,proto3" json:"network,omitempty"`
-	// Timeout after which the container will be killed.
-	Timeout *durationpb.Duration `protobuf:"bytes,3,opt,name=timeout,proto3" json:"timeout,omitempty"`
-}
-
-func (x *RunFunctionConfig) Reset() {
-	*x = RunFunctionConfig{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[5]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *RunFunctionConfig) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*RunFunctionConfig) ProtoMessage() {}
-
-func (x *RunFunctionConfig) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[5]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use RunFunctionConfig.ProtoReflect.Descriptor instead.
-func (*RunFunctionConfig) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{5}
-}
-
-func (x *RunFunctionConfig) GetResources() *ResourceConfig {
-	if x != nil {
-		return x.Resources
-	}
-	return nil
-}
-
-func (x *RunFunctionConfig) GetNetwork() *NetworkConfig {
-	if x != nil {
-		return x.Network
-	}
-	return nil
-}
-
-func (x *RunFunctionConfig) GetTimeout() *durationpb.Duration {
-	if x != nil {
-		return x.Timeout
-	}
-	return nil
-}
-
-// A RunFunctionRequest requests that a Composition Function be run.
-type RunFunctionRequest struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	// OCI image of the Composition Function.
-	Image string `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"`
-	// A FunctionIO serialized as YAML.
-	Input []byte `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"`
-	// Configures how the function image is pulled.
-	ImagePullConfig *ImagePullConfig `protobuf:"bytes,3,opt,name=image_pull_config,json=imagePullConfig,proto3" json:"image_pull_config,omitempty"`
-	// Configures how the function container is run.
-	RunFunctionConfig *RunFunctionConfig `protobuf:"bytes,4,opt,name=run_function_config,json=runFunctionConfig,proto3" json:"run_function_config,omitempty"`
-}
-
-func (x *RunFunctionRequest) Reset() {
-	*x = RunFunctionRequest{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[6]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *RunFunctionRequest) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*RunFunctionRequest) ProtoMessage() {}
-
-func (x *RunFunctionRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[6]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use RunFunctionRequest.ProtoReflect.Descriptor instead.
-func (*RunFunctionRequest) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{6}
-}
-
-func (x *RunFunctionRequest) GetImage() string {
-	if x != nil {
-		return x.Image
-	}
-	return ""
-}
-
-func (x *RunFunctionRequest) GetInput() []byte {
-	if x != nil {
-		return x.Input
-	}
-	return nil
-}
-
-func (x *RunFunctionRequest) GetImagePullConfig() *ImagePullConfig {
-	if x != nil {
-		return x.ImagePullConfig
-	}
-	return nil
-}
-
-func (x *RunFunctionRequest) GetRunFunctionConfig() *RunFunctionConfig {
-	if x != nil {
-		return x.RunFunctionConfig
-	}
-	return nil
-}
-
-// A RunFunctionResponse contains the response from a Composition Function run.
-// The output FunctionIO is returned as opaque bytes. Errors encountered while
-// running a function (as opposed to errors returned _by_ a function) will be
-// encapsulated as gRPC errors.
-type RunFunctionResponse struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	Output []byte `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"`
-}
-
-func (x *RunFunctionResponse) Reset() {
-	*x = RunFunctionResponse{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[7]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *RunFunctionResponse) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*RunFunctionResponse) ProtoMessage() {}
-
-func (x *RunFunctionResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[7]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use RunFunctionResponse.ProtoReflect.Descriptor instead.
-func (*RunFunctionResponse) Descriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP(), []int{7}
-}
-
-func (x *RunFunctionResponse) GetOutput() []byte {
-	if x != nil {
-		return x.Output
-	}
-	return nil
-}
-
-var File_apiextensions_fn_proto_v1alpha1_run_function_proto protoreflect.FileDescriptor
-
-var file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDesc = []byte{
-	0x0a, 0x32, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f,
-	0x66, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
-	0x31, 0x2f, 0x72, 0x75, 0x6e, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69,
-	0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61,
-	0x6c, 0x70, 0x68, 0x61, 0x31, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa9, 0x01, 0x0a, 0x0d, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50,
-	0x75, 0x6c, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
-	0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e,
-	0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18,
-	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12,
-	0x12, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61,
-	0x75, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f,
-	0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x64, 0x65,
-	0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65,
-	0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x05, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x54, 0x6f, 0x6b, 0x65,
-	0x6e, 0x22, 0xa8, 0x01, 0x0a, 0x0f, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x43,
-	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x51, 0x0a, 0x0b, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x70, 0x6f,
-	0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x61, 0x70, 0x69,
-	0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72,
-	0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x49, 0x6d, 0x61,
-	0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0a, 0x70, 0x75,
-	0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x42, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65,
-	0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
-	0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75,
-	0x6c, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x57, 0x0a, 0x0d,
-	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x46, 0x0a,
-	0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2e, 0x2e,
-	0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e,
-	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x70,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x59, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
-	0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x47, 0x0a, 0x06, 0x6c, 0x69, 0x6d, 0x69, 0x74,
-	0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74,
-	0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
-	0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x73, 0x52, 0x06, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x73,
-	0x22, 0x3a, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4c, 0x69, 0x6d, 0x69,
-	0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01,
-	0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x70,
-	0x75, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x63, 0x70, 0x75, 0x22, 0xe1, 0x01, 0x0a,
-	0x11, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
-	0x69, 0x67, 0x12, 0x4d, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e,
-	0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76,
-	0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
-	0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
-	0x73, 0x12, 0x48, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
-	0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f,
-	0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c,
-	0x70, 0x68, 0x61, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66,
-	0x69, 0x67, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x33, 0x0a, 0x07, 0x74,
-	0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67,
-	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44,
-	0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
-	0x22, 0x82, 0x02, 0x0a, 0x12, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65,
-	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a,
-	0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x69, 0x6e,
-	0x70, 0x75, 0x74, 0x12, 0x5c, 0x0a, 0x11, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x75, 0x6c,
-	0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30,
-	0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66,
-	0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
-	0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
-	0x52, 0x0f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69,
-	0x67, 0x12, 0x62, 0x0a, 0x13, 0x72, 0x75, 0x6e, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f,
-	0x6e, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32,
-	0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66,
-	0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31,
-	0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
-	0x69, 0x67, 0x52, 0x11, 0x72, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43,
-	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x2d, 0x0a, 0x13, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63,
-	0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06,
-	0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x6f, 0x75,
-	0x74, 0x70, 0x75, 0x74, 0x2a, 0x95, 0x01, 0x0a, 0x0f, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x50, 0x75,
-	0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x21, 0x0a, 0x1d, 0x49, 0x4d, 0x41, 0x47,
-	0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e,
-	0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x24, 0x0a, 0x20, 0x49,
-	0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59,
-	0x5f, 0x49, 0x46, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x50, 0x52, 0x45, 0x53, 0x45, 0x4e, 0x54, 0x10,
-	0x01, 0x12, 0x1c, 0x0a, 0x18, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f,
-	0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x02, 0x12,
-	0x1b, 0x0a, 0x17, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f, 0x50, 0x4f,
-	0x4c, 0x49, 0x43, 0x59, 0x5f, 0x4e, 0x45, 0x56, 0x45, 0x52, 0x10, 0x03, 0x2a, 0x67, 0x0a, 0x0d,
-	0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1e, 0x0a,
-	0x1a, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f,
-	0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a,
-	0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f,
-	0x49, 0x53, 0x4f, 0x4c, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x4e, 0x45,
-	0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x52, 0x55, 0x4e,
-	0x4e, 0x45, 0x52, 0x10, 0x02, 0x32, 0xa0, 0x01, 0x0a, 0x22, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69,
-	0x6e, 0x65, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
-	0x75, 0x6e, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7a, 0x0a, 0x0b,
-	0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x2e, 0x61, 0x70,
-	0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70,
-	0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x52, 0x75,
-	0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x1a, 0x34, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73,
-	0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
-	0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
-	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68,
-	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e,
-	0x65, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69,
-	0x73, 0x2f, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f,
-	0x66, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61,
-	0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
-}
-
-var (
-	file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescOnce sync.Once
-	file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescData = file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDesc
-)
-
-func file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescGZIP() []byte {
-	file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescOnce.Do(func() {
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescData = protoimpl.X.CompressGZIP(file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescData)
-	})
-	return file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDescData
-}
-
-var file_apiextensions_fn_proto_v1alpha1_run_function_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
-var file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
-var file_apiextensions_fn_proto_v1alpha1_run_function_proto_goTypes = []interface{}{
-	(ImagePullPolicy)(0),        // 0: apiextensions.fn.proto.v1alpha1.ImagePullPolicy
-	(NetworkPolicy)(0),          // 1: apiextensions.fn.proto.v1alpha1.NetworkPolicy
-	(*ImagePullAuth)(nil),       // 2: apiextensions.fn.proto.v1alpha1.ImagePullAuth
-	(*ImagePullConfig)(nil),     // 3: apiextensions.fn.proto.v1alpha1.ImagePullConfig
-	(*NetworkConfig)(nil),       // 4: apiextensions.fn.proto.v1alpha1.NetworkConfig
-	(*ResourceConfig)(nil),      // 5: apiextensions.fn.proto.v1alpha1.ResourceConfig
-	(*ResourceLimits)(nil),      // 6: apiextensions.fn.proto.v1alpha1.ResourceLimits
-	(*RunFunctionConfig)(nil),   // 7: apiextensions.fn.proto.v1alpha1.RunFunctionConfig
-	(*RunFunctionRequest)(nil),  // 8: apiextensions.fn.proto.v1alpha1.RunFunctionRequest
-	(*RunFunctionResponse)(nil), // 9: apiextensions.fn.proto.v1alpha1.RunFunctionResponse
-	(*durationpb.Duration)(nil), // 10: google.protobuf.Duration
-}
-var file_apiextensions_fn_proto_v1alpha1_run_function_proto_depIdxs = []int32{
-	0,  // 0: apiextensions.fn.proto.v1alpha1.ImagePullConfig.pull_policy:type_name -> apiextensions.fn.proto.v1alpha1.ImagePullPolicy
-	2,  // 1: apiextensions.fn.proto.v1alpha1.ImagePullConfig.auth:type_name -> apiextensions.fn.proto.v1alpha1.ImagePullAuth
-	1,  // 2: apiextensions.fn.proto.v1alpha1.NetworkConfig.policy:type_name -> apiextensions.fn.proto.v1alpha1.NetworkPolicy
-	6,  // 3: apiextensions.fn.proto.v1alpha1.ResourceConfig.limits:type_name -> apiextensions.fn.proto.v1alpha1.ResourceLimits
-	5,  // 4: apiextensions.fn.proto.v1alpha1.RunFunctionConfig.resources:type_name -> apiextensions.fn.proto.v1alpha1.ResourceConfig
-	4,  // 5: apiextensions.fn.proto.v1alpha1.RunFunctionConfig.network:type_name -> apiextensions.fn.proto.v1alpha1.NetworkConfig
-	10, // 6: apiextensions.fn.proto.v1alpha1.RunFunctionConfig.timeout:type_name -> google.protobuf.Duration
-	3,  // 7: apiextensions.fn.proto.v1alpha1.RunFunctionRequest.image_pull_config:type_name -> apiextensions.fn.proto.v1alpha1.ImagePullConfig
-	7,  // 8: apiextensions.fn.proto.v1alpha1.RunFunctionRequest.run_function_config:type_name -> apiextensions.fn.proto.v1alpha1.RunFunctionConfig
-	8,  // 9: apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService.RunFunction:input_type -> apiextensions.fn.proto.v1alpha1.RunFunctionRequest
-	9,  // 10: apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService.RunFunction:output_type -> apiextensions.fn.proto.v1alpha1.RunFunctionResponse
-	10, // [10:11] is the sub-list for method output_type
-	9,  // [9:10] is the sub-list for method input_type
-	9,  // [9:9] is the sub-list for extension type_name
-	9,  // [9:9] is the sub-list for extension extendee
-	0,  // [0:9] is the sub-list for field type_name
-}
-
-func init() { file_apiextensions_fn_proto_v1alpha1_run_function_proto_init() }
-func file_apiextensions_fn_proto_v1alpha1_run_function_proto_init() {
-	if File_apiextensions_fn_proto_v1alpha1_run_function_proto != nil {
-		return
-	}
-	if !protoimpl.UnsafeEnabled {
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ImagePullAuth); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ImagePullConfig); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*NetworkConfig); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ResourceConfig); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*ResourceLimits); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RunFunctionConfig); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RunFunctionRequest); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-		file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*RunFunctionResponse); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-	}
-	type x struct{}
-	out := protoimpl.TypeBuilder{
-		File: protoimpl.DescBuilder{
-			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDesc,
-			NumEnums:      2,
-			NumMessages:   8,
-			NumExtensions: 0,
-			NumServices:   1,
-		},
-		GoTypes:           file_apiextensions_fn_proto_v1alpha1_run_function_proto_goTypes,
-		DependencyIndexes: file_apiextensions_fn_proto_v1alpha1_run_function_proto_depIdxs,
-		EnumInfos:         file_apiextensions_fn_proto_v1alpha1_run_function_proto_enumTypes,
-		MessageInfos:      file_apiextensions_fn_proto_v1alpha1_run_function_proto_msgTypes,
-	}.Build()
-	File_apiextensions_fn_proto_v1alpha1_run_function_proto = out.File
-	file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDesc = nil
-	file_apiextensions_fn_proto_v1alpha1_run_function_proto_goTypes = nil
-	file_apiextensions_fn_proto_v1alpha1_run_function_proto_depIdxs = nil
-}
diff --git a/apis/apiextensions/fn/proto/v1alpha1/run_function.proto b/apis/apiextensions/fn/proto/v1alpha1/run_function.proto
deleted file mode 100644
index ff4484497..000000000
--- a/apis/apiextensions/fn/proto/v1alpha1/run_function.proto
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
-Copyright 2022 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-// TODO(negz): Add a make target such that `make lint` runs `buf lint`.
-
-syntax = "proto3";
-
-import "google/protobuf/duration.proto";
-
-package apiextensions.fn.proto.v1alpha1;
-
-option go_package = "github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1alpha1";
-
-// A ContainerizedFunctionRunnerService runs containerized Composition Functions.
-service ContainerizedFunctionRunnerService {
-    // RunFunction runs a containerized function.
-    rpc RunFunction(RunFunctionRequest) returns (RunFunctionResponse) {}
-}
-
-// ImagePullPolicy specifies when a Composition Function container should be
-// pulled from a remote OCI registry.
-enum ImagePullPolicy {
-  IMAGE_PULL_POLICY_UNSPECIFIED = 0;
-  IMAGE_PULL_POLICY_IF_NOT_PRESENT = 1;
-  IMAGE_PULL_POLICY_ALWAYS = 2;
-  IMAGE_PULL_POLICY_NEVER = 3;
-}
-
-// ImagePullAuth configures authentication to a remote OCI registry.
-// It corresponds to go-containerregistry's AuthConfig type.
-// https://pkg.go.dev/github.com/google/go-containerregistry@v0.11.0/pkg/authn#AuthConfig
-message ImagePullAuth {
-  string username = 1;
-  string password = 2;
-  string auth = 3;
-  string identity_token = 4;
-  string registry_token = 5;
-}
-
-// ImagePullConfig configures how a Composition Function container should be
-// pulled from a remote OCI registry.
-message ImagePullConfig {
-  ImagePullPolicy pull_policy = 1;
-  ImagePullAuth auth = 2;
-}
-
-// NetworkPolicy configures whether a container is isolated from the network.
-enum NetworkPolicy {
-  NETWORK_POLICY_UNSPECIFIED = 0;
-
-  // Run the container without network access. The default.
-  NETWORK_POLICY_ISOLATED = 1;
-
-  // Allow the container to access the same network as the function runner.
-  NETWORK_POLICY_RUNNER = 2;
-}
-
-// NetworkConfig configures whether and how a Composition Function container may
-// access the network.
-message NetworkConfig {
-  // Whether or not the container can access the network.
-  NetworkPolicy policy = 1;
-}
-
-// Resources configures what compute resources should be available to a
-// Composition Function container.
-message ResourceConfig {
-  ResourceLimits limits = 1;
-}
-
-// ResourceLimits configures the maximum compute resources that will be
-// available to a Composition Function container.
-message ResourceLimits {
-  // CPU, in cores. (500m = .5 cores)
-  // Specified in Kubernetes-style resource.Quantity form.
-  string memory = 1;
-
-  // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
-  // Specified in Kubernetes-style resource.Quantity form.
-  string cpu = 2;
-}
-
-// RunFunctionConfig configures how a Composition Function container is run.
-message RunFunctionConfig {
-  // Resources available to the container.
-  ResourceConfig resources = 1;
-
-  // Network configuration for the container.
-  NetworkConfig network = 2;
-
-  // Timeout after which the container will be killed.
-  google.protobuf.Duration timeout = 3;
-}
-
-// A RunFunctionRequest requests that a Composition Function be run.
-message RunFunctionRequest {
-  // OCI image of the Composition Function.
-  string image = 1;
-
-  // A FunctionIO serialized as YAML.
-  bytes input = 2;
-
-  // Configures how the function image is pulled.
-  ImagePullConfig image_pull_config = 3;
-
-  // Configures how the function container is run.
-  RunFunctionConfig run_function_config = 4;
-}
-
-// A RunFunctionResponse contains the response from a Composition Function run.
-// The output FunctionIO is returned as opaque bytes. Errors encountered while
-// running a function (as opposed to errors returned _by_ a function) will be
-// encapsulated as gRPC errors.
-message RunFunctionResponse {
-  bytes output = 1;
-}
diff --git a/apis/apiextensions/fn/proto/v1alpha1/run_function_grpc.pb.go b/apis/apiextensions/fn/proto/v1alpha1/run_function_grpc.pb.go
deleted file mode 100644
index 9a79dff96..000000000
--- a/apis/apiextensions/fn/proto/v1alpha1/run_function_grpc.pb.go
+++ /dev/null
@@ -1,129 +0,0 @@
-//
-//Copyright 2022 The Crossplane Authors.
-//
-//Licensed under the Apache License, Version 2.0 (the "License");
-//you may not use this file except in compliance with the License.
-//You may obtain a copy of the License at
-//
-//http://www.apache.org/licenses/LICENSE-2.0
-//
-//Unless required by applicable law or agreed to in writing, software
-//distributed under the License is distributed on an "AS IS" BASIS,
-//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-//See the License for the specific language governing permissions and
-//limitations under the License.
-
-// TODO(negz): Add a make target such that `make lint` runs `buf lint`.
-
-// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
-// versions:
-// - protoc-gen-go-grpc v1.3.0
-// - protoc             (unknown)
-// source: apiextensions/fn/proto/v1alpha1/run_function.proto
-
-package v1alpha1
-
-import (
-	context "context"
-	grpc "google.golang.org/grpc"
-	codes "google.golang.org/grpc/codes"
-	status "google.golang.org/grpc/status"
-)
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the grpc package it is being compiled against.
-// Requires gRPC-Go v1.32.0 or later.
-const _ = grpc.SupportPackageIsVersion7
-
-const (
-	ContainerizedFunctionRunnerService_RunFunction_FullMethodName = "/apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService/RunFunction"
-)
-
-// ContainerizedFunctionRunnerServiceClient is the client API for ContainerizedFunctionRunnerService service.
-//
-// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
-type ContainerizedFunctionRunnerServiceClient interface {
-	// RunFunction runs a containerized function.
-	RunFunction(ctx context.Context, in *RunFunctionRequest, opts ...grpc.CallOption) (*RunFunctionResponse, error)
-}
-
-type containerizedFunctionRunnerServiceClient struct {
-	cc grpc.ClientConnInterface
-}
-
-func NewContainerizedFunctionRunnerServiceClient(cc grpc.ClientConnInterface) ContainerizedFunctionRunnerServiceClient {
-	return &containerizedFunctionRunnerServiceClient{cc}
-}
-
-func (c *containerizedFunctionRunnerServiceClient) RunFunction(ctx context.Context, in *RunFunctionRequest, opts ...grpc.CallOption) (*RunFunctionResponse, error) {
-	out := new(RunFunctionResponse)
-	err := c.cc.Invoke(ctx, ContainerizedFunctionRunnerService_RunFunction_FullMethodName, in, out, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-// ContainerizedFunctionRunnerServiceServer is the server API for ContainerizedFunctionRunnerService service.
-// All implementations must embed UnimplementedContainerizedFunctionRunnerServiceServer
-// for forward compatibility
-type ContainerizedFunctionRunnerServiceServer interface {
-	// RunFunction runs a containerized function.
-	RunFunction(context.Context, *RunFunctionRequest) (*RunFunctionResponse, error)
-	mustEmbedUnimplementedContainerizedFunctionRunnerServiceServer()
-}
-
-// UnimplementedContainerizedFunctionRunnerServiceServer must be embedded to have forward compatible implementations.
-type UnimplementedContainerizedFunctionRunnerServiceServer struct {
-}
-
-func (UnimplementedContainerizedFunctionRunnerServiceServer) RunFunction(context.Context, *RunFunctionRequest) (*RunFunctionResponse, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method RunFunction not implemented")
-}
-func (UnimplementedContainerizedFunctionRunnerServiceServer) mustEmbedUnimplementedContainerizedFunctionRunnerServiceServer() {
-}
-
-// UnsafeContainerizedFunctionRunnerServiceServer may be embedded to opt out of forward compatibility for this service.
-// Use of this interface is not recommended, as added methods to ContainerizedFunctionRunnerServiceServer will
-// result in compilation errors.
-type UnsafeContainerizedFunctionRunnerServiceServer interface {
-	mustEmbedUnimplementedContainerizedFunctionRunnerServiceServer()
-}
-
-func RegisterContainerizedFunctionRunnerServiceServer(s grpc.ServiceRegistrar, srv ContainerizedFunctionRunnerServiceServer) {
-	s.RegisterService(&ContainerizedFunctionRunnerService_ServiceDesc, srv)
-}
-
-func _ContainerizedFunctionRunnerService_RunFunction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(RunFunctionRequest)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(ContainerizedFunctionRunnerServiceServer).RunFunction(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: ContainerizedFunctionRunnerService_RunFunction_FullMethodName,
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(ContainerizedFunctionRunnerServiceServer).RunFunction(ctx, req.(*RunFunctionRequest))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-// ContainerizedFunctionRunnerService_ServiceDesc is the grpc.ServiceDesc for ContainerizedFunctionRunnerService service.
-// It's only intended for direct use with grpc.RegisterService,
-// and not to be introspected or modified (even as a copy)
-var ContainerizedFunctionRunnerService_ServiceDesc = grpc.ServiceDesc{
-	ServiceName: "apiextensions.fn.proto.v1alpha1.ContainerizedFunctionRunnerService",
-	HandlerType: (*ContainerizedFunctionRunnerServiceServer)(nil),
-	Methods: []grpc.MethodDesc{
-		{
-			MethodName: "RunFunction",
-			Handler:    _ContainerizedFunctionRunnerService_RunFunction_Handler,
-		},
-	},
-	Streams:  []grpc.StreamDesc{},
-	Metadata: "apiextensions/fn/proto/v1alpha1/run_function.proto",
-}
diff --git a/apis/apiextensions/fn/proto/v1beta1/run_function.pb.go b/apis/apiextensions/fn/proto/v1beta1/run_function.pb.go
index 59035f05e..eae54c5fb 100644
--- a/apis/apiextensions/fn/proto/v1beta1/run_function.pb.go
+++ b/apis/apiextensions/fn/proto/v1beta1/run_function.pb.go
@@ -37,6 +37,58 @@ const (
 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 )
 
+// Ready indicates whether a composed resource should be considered ready.
+type Ready int32
+
+const (
+	Ready_READY_UNSPECIFIED Ready = 0
+	// True means the composed resource has been observed to be ready.
+	Ready_READY_TRUE Ready = 1
+	// False means the composed resource has not been observed to be ready.
+	Ready_READY_FALSE Ready = 2
+)
+
+// Enum value maps for Ready.
+var (
+	Ready_name = map[int32]string{
+		0: "READY_UNSPECIFIED",
+		1: "READY_TRUE",
+		2: "READY_FALSE",
+	}
+	Ready_value = map[string]int32{
+		"READY_UNSPECIFIED": 0,
+		"READY_TRUE":        1,
+		"READY_FALSE":       2,
+	}
+)
+
+func (x Ready) Enum() *Ready {
+	p := new(Ready)
+	*p = x
+	return p
+}
+
+func (x Ready) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (Ready) Descriptor() protoreflect.EnumDescriptor {
+	return file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes[0].Descriptor()
+}
+
+func (Ready) Type() protoreflect.EnumType {
+	return &file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes[0]
+}
+
+func (x Ready) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use Ready.Descriptor instead.
+func (Ready) EnumDescriptor() ([]byte, []int) {
+	return file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDescGZIP(), []int{0}
+}
+
 // Severity of Function results.
 type Severity int32
 
@@ -82,11 +134,11 @@ func (x Severity) String() string {
 }
 
 func (Severity) Descriptor() protoreflect.EnumDescriptor {
-	return file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes[0].Descriptor()
+	return file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes[1].Descriptor()
 }
 
 func (Severity) Type() protoreflect.EnumType {
-	return &file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes[0]
+	return &file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes[1]
 }
 
 func (x Severity) Number() protoreflect.EnumNumber {
@@ -95,7 +147,7 @@ func (x Severity) Number() protoreflect.EnumNumber {
 
 // Deprecated: Use Severity.Descriptor instead.
 func (Severity) EnumDescriptor() ([]byte, []int) {
-	return file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDescGZIP(), []int{0}
+	return file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDescGZIP(), []int{1}
 }
 
 // A RunFunctionRequest requests that the Composition Function be run.
@@ -111,9 +163,8 @@ type RunFunctionRequest struct {
 	// of the time each Function was invoked.
 	Observed *State `protobuf:"bytes,2,opt,name=observed,proto3" json:"observed,omitempty"`
 	// Desired state according to a Function pipeline. The state passed to a
-	// particular Function may have been accumulated by processing a Composition's
-	// patch-and-transform resources array. It may also have been accumulated by
-	// previous Functions in the pipeline.
+	// particular Function may have been accumulated by previous Functions in the
+	// pipeline.
 	Desired *State `protobuf:"bytes,3,opt,name=desired,proto3" json:"desired,omitempty"`
 	// Optional input specific to this Function invocation. A JSON representation
 	// of the 'input' block of the relevant entry in a Composition's pipeline.
@@ -419,16 +470,50 @@ func (x *State) GetResources() map[string]*Resource {
 	return nil
 }
 
-// A Resource represents the state of a resource.
+// A Resource represents the state of a composite or composed resource.
 type Resource struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
 	unknownFields protoimpl.UnknownFields
 
 	// The JSON representation of the resource.
+	//
+	//   - Crossplane will set this field in a RunFunctionRequest to the entire
+	//     observed state of a resource - including its metadata, spec, and status.
+	//
+	//   - A Function should set this field in a RunFunctionRequest to communicate
+	//     the desired state of a composite or composed resource.
+	//
+	//   - A Function may only specify the desired status of a composite resource -
+	//     not its metadata or spec. A Function should not return desired metadata
+	//     or spec for a composite resource. This will be ignored.
+	//
+	//   - A Function may not specify the desired status of a composed resource -
+	//     only its metadata and spec. A Function should not return desired status
+	//     for a composed resource. This will be ignored.
 	Resource *structpb.Struct `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"`
 	// The resource's connection details.
+	//
+	//   - Crossplane will set this field in a RunFunctionRequest to communicate the
+	//     the observed connection details of a composite or composed resource.
+	//
+	//   - A Function should set this field in a RunFunctionResponse to indicate the
+	//     desired connection details of the composite resource.
+	//
+	//   - A Function should not set this field in a RunFunctionResponse to indicate
+	//     the desired connection details of a composed resource. This will be
+	//     ignored.
 	ConnectionDetails map[string][]byte `protobuf:"bytes,2,rep,name=connection_details,json=connectionDetails,proto3" json:"connection_details,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	// Ready indicates whether the resource should be considered ready.
+	//
+	// * Crossplane will never set this field in a RunFunctionRequest.
+	//
+	//   - A Function should set this field to READY_TRUE in a RunFunctionResponse
+	//     to indicate that a desired composed resource is ready.
+	//
+	//   - A Function should not set this field in a RunFunctionResponse to indicate
+	//     that the desired composite resource is ready. This will be ignored.
+	Ready Ready `protobuf:"varint,3,opt,name=ready,proto3,enum=apiextensions.fn.proto.v1beta1.Ready" json:"ready,omitempty"`
 }
 
 func (x *Resource) Reset() {
@@ -477,6 +562,13 @@ func (x *Resource) GetConnectionDetails() map[string][]byte {
 	return nil
 }
 
+func (x *Resource) GetReady() Ready {
+	if x != nil {
+		return x.Ready
+	}
+	return Ready_READY_UNSPECIFIED
+}
+
 // A Result of running a Function.
 type Result struct {
 	state         protoimpl.MessageState
@@ -603,7 +695,7 @@ var file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDesc = []byte{
 	0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66,
 	0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e,
 	0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a,
-	0x02, 0x38, 0x01, 0x22, 0xf5, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+	0x02, 0x38, 0x01, 0x22, 0xb2, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
 	0x12, 0x33, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01,
 	0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
 	0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x08, 0x72, 0x65, 0x73,
@@ -614,38 +706,46 @@ var file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDesc = []byte{
 	0x61, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x6e,
 	0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x45, 0x6e, 0x74,
 	0x72, 0x79, 0x52, 0x11, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65,
-	0x74, 0x61, 0x69, 0x6c, 0x73, 0x1a, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
-	0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
-	0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
-	0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
-	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x68, 0x0a, 0x06, 0x52,
-	0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x44, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74,
-	0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74,
-	0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74,
-	0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d,
-	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65,
-	0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0x63, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74,
-	0x79, 0x12, 0x18, 0x0a, 0x14, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e,
-	0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53,
-	0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x01, 0x12,
-	0x14, 0x0a, 0x10, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x57, 0x41, 0x52, 0x4e,
-	0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54,
-	0x59, 0x5f, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x03, 0x32, 0x91, 0x01, 0x0a, 0x15, 0x46,
-	0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x72,
-	0x76, 0x69, 0x63, 0x65, 0x12, 0x78, 0x0a, 0x0b, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74,
-	0x69, 0x6f, 0x6e, 0x12, 0x32, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69,
-	0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x62,
-	0x65, 0x74, 0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74,
-	0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63,
-	0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x46,
-	0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6f,
-	0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61,
-	0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e,
-	0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x66, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76,
-	0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x3b, 0x0a, 0x05, 0x72, 0x65, 0x61, 0x64, 0x79, 0x18, 0x03,
+	0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
+	0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31,
+	0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x79, 0x52, 0x05, 0x72, 0x65, 0x61,
+	0x64, 0x79, 0x1a, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+	0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
+	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14,
+	0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76,
+	0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x68, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75,
+	0x6c, 0x74, 0x12, 0x44, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73,
+	0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31,
+	0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08,
+	0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73,
+	0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
+	0x67, 0x65, 0x2a, 0x3f, 0x0a, 0x05, 0x52, 0x65, 0x61, 0x64, 0x79, 0x12, 0x15, 0x0a, 0x11, 0x52,
+	0x45, 0x41, 0x44, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44,
+	0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x45, 0x41, 0x44, 0x59, 0x5f, 0x54, 0x52, 0x55, 0x45,
+	0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x52, 0x45, 0x41, 0x44, 0x59, 0x5f, 0x46, 0x41, 0x4c, 0x53,
+	0x45, 0x10, 0x02, 0x2a, 0x63, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12,
+	0x18, 0x0a, 0x14, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50,
+	0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x45, 0x56,
+	0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x01, 0x12, 0x14, 0x0a,
+	0x10, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e,
+	0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x56, 0x45, 0x52, 0x49, 0x54, 0x59, 0x5f,
+	0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x03, 0x32, 0x91, 0x01, 0x0a, 0x15, 0x46, 0x75, 0x6e,
+	0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69,
+	0x63, 0x65, 0x12, 0x78, 0x0a, 0x0b, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f,
+	0x6e, 0x12, 0x32, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e,
+	0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74,
+	0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e,
+	0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x66, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x76,
+	0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69,
+	0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x46, 0x5a, 0x44,
+	0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73,
+	0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65,
+	0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69,
+	0x6f, 0x6e, 0x73, 0x2f, 0x66, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62,
+	0x65, 0x74, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -660,44 +760,46 @@ func file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDescGZIP() []byte
 	return file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDescData
 }
 
-var file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_apiextensions_fn_proto_v1beta1_run_function_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
 var file_apiextensions_fn_proto_v1beta1_run_function_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
 var file_apiextensions_fn_proto_v1beta1_run_function_proto_goTypes = []interface{}{
-	(Severity)(0),               // 0: apiextensions.fn.proto.v1beta1.Severity
-	(*RunFunctionRequest)(nil),  // 1: apiextensions.fn.proto.v1beta1.RunFunctionRequest
-	(*RunFunctionResponse)(nil), // 2: apiextensions.fn.proto.v1beta1.RunFunctionResponse
-	(*RequestMeta)(nil),         // 3: apiextensions.fn.proto.v1beta1.RequestMeta
-	(*ResponseMeta)(nil),        // 4: apiextensions.fn.proto.v1beta1.ResponseMeta
-	(*State)(nil),               // 5: apiextensions.fn.proto.v1beta1.State
-	(*Resource)(nil),            // 6: apiextensions.fn.proto.v1beta1.Resource
-	(*Result)(nil),              // 7: apiextensions.fn.proto.v1beta1.Result
-	nil,                         // 8: apiextensions.fn.proto.v1beta1.State.ResourcesEntry
-	nil,                         // 9: apiextensions.fn.proto.v1beta1.Resource.ConnectionDetailsEntry
-	(*structpb.Struct)(nil),     // 10: google.protobuf.Struct
-	(*durationpb.Duration)(nil), // 11: google.protobuf.Duration
+	(Ready)(0),                  // 0: apiextensions.fn.proto.v1beta1.Ready
+	(Severity)(0),               // 1: apiextensions.fn.proto.v1beta1.Severity
+	(*RunFunctionRequest)(nil),  // 2: apiextensions.fn.proto.v1beta1.RunFunctionRequest
+	(*RunFunctionResponse)(nil), // 3: apiextensions.fn.proto.v1beta1.RunFunctionResponse
+	(*RequestMeta)(nil),         // 4: apiextensions.fn.proto.v1beta1.RequestMeta
+	(*ResponseMeta)(nil),        // 5: apiextensions.fn.proto.v1beta1.ResponseMeta
+	(*State)(nil),               // 6: apiextensions.fn.proto.v1beta1.State
+	(*Resource)(nil),            // 7: apiextensions.fn.proto.v1beta1.Resource
+	(*Result)(nil),              // 8: apiextensions.fn.proto.v1beta1.Result
+	nil,                         // 9: apiextensions.fn.proto.v1beta1.State.ResourcesEntry
+	nil,                         // 10: apiextensions.fn.proto.v1beta1.Resource.ConnectionDetailsEntry
+	(*structpb.Struct)(nil),     // 11: google.protobuf.Struct
+	(*durationpb.Duration)(nil), // 12: google.protobuf.Duration
 }
 var file_apiextensions_fn_proto_v1beta1_run_function_proto_depIdxs = []int32{
-	3,  // 0: apiextensions.fn.proto.v1beta1.RunFunctionRequest.meta:type_name -> apiextensions.fn.proto.v1beta1.RequestMeta
-	5,  // 1: apiextensions.fn.proto.v1beta1.RunFunctionRequest.observed:type_name -> apiextensions.fn.proto.v1beta1.State
-	5,  // 2: apiextensions.fn.proto.v1beta1.RunFunctionRequest.desired:type_name -> apiextensions.fn.proto.v1beta1.State
-	10, // 3: apiextensions.fn.proto.v1beta1.RunFunctionRequest.input:type_name -> google.protobuf.Struct
-	4,  // 4: apiextensions.fn.proto.v1beta1.RunFunctionResponse.meta:type_name -> apiextensions.fn.proto.v1beta1.ResponseMeta
-	5,  // 5: apiextensions.fn.proto.v1beta1.RunFunctionResponse.desired:type_name -> apiextensions.fn.proto.v1beta1.State
-	7,  // 6: apiextensions.fn.proto.v1beta1.RunFunctionResponse.results:type_name -> apiextensions.fn.proto.v1beta1.Result
-	11, // 7: apiextensions.fn.proto.v1beta1.ResponseMeta.ttl:type_name -> google.protobuf.Duration
-	6,  // 8: apiextensions.fn.proto.v1beta1.State.composite:type_name -> apiextensions.fn.proto.v1beta1.Resource
-	8,  // 9: apiextensions.fn.proto.v1beta1.State.resources:type_name -> apiextensions.fn.proto.v1beta1.State.ResourcesEntry
-	10, // 10: apiextensions.fn.proto.v1beta1.Resource.resource:type_name -> google.protobuf.Struct
-	9,  // 11: apiextensions.fn.proto.v1beta1.Resource.connection_details:type_name -> apiextensions.fn.proto.v1beta1.Resource.ConnectionDetailsEntry
-	0,  // 12: apiextensions.fn.proto.v1beta1.Result.severity:type_name -> apiextensions.fn.proto.v1beta1.Severity
-	6,  // 13: apiextensions.fn.proto.v1beta1.State.ResourcesEntry.value:type_name -> apiextensions.fn.proto.v1beta1.Resource
-	1,  // 14: apiextensions.fn.proto.v1beta1.FunctionRunnerService.RunFunction:input_type -> apiextensions.fn.proto.v1beta1.RunFunctionRequest
-	2,  // 15: apiextensions.fn.proto.v1beta1.FunctionRunnerService.RunFunction:output_type -> apiextensions.fn.proto.v1beta1.RunFunctionResponse
-	15, // [15:16] is the sub-list for method output_type
-	14, // [14:15] is the sub-list for method input_type
-	14, // [14:14] is the sub-list for extension type_name
-	14, // [14:14] is the sub-list for extension extendee
-	0,  // [0:14] is the sub-list for field type_name
+	4,  // 0: apiextensions.fn.proto.v1beta1.RunFunctionRequest.meta:type_name -> apiextensions.fn.proto.v1beta1.RequestMeta
+	6,  // 1: apiextensions.fn.proto.v1beta1.RunFunctionRequest.observed:type_name -> apiextensions.fn.proto.v1beta1.State
+	6,  // 2: apiextensions.fn.proto.v1beta1.RunFunctionRequest.desired:type_name -> apiextensions.fn.proto.v1beta1.State
+	11, // 3: apiextensions.fn.proto.v1beta1.RunFunctionRequest.input:type_name -> google.protobuf.Struct
+	5,  // 4: apiextensions.fn.proto.v1beta1.RunFunctionResponse.meta:type_name -> apiextensions.fn.proto.v1beta1.ResponseMeta
+	6,  // 5: apiextensions.fn.proto.v1beta1.RunFunctionResponse.desired:type_name -> apiextensions.fn.proto.v1beta1.State
+	8,  // 6: apiextensions.fn.proto.v1beta1.RunFunctionResponse.results:type_name -> apiextensions.fn.proto.v1beta1.Result
+	12, // 7: apiextensions.fn.proto.v1beta1.ResponseMeta.ttl:type_name -> google.protobuf.Duration
+	7,  // 8: apiextensions.fn.proto.v1beta1.State.composite:type_name -> apiextensions.fn.proto.v1beta1.Resource
+	9,  // 9: apiextensions.fn.proto.v1beta1.State.resources:type_name -> apiextensions.fn.proto.v1beta1.State.ResourcesEntry
+	11, // 10: apiextensions.fn.proto.v1beta1.Resource.resource:type_name -> google.protobuf.Struct
+	10, // 11: apiextensions.fn.proto.v1beta1.Resource.connection_details:type_name -> apiextensions.fn.proto.v1beta1.Resource.ConnectionDetailsEntry
+	0,  // 12: apiextensions.fn.proto.v1beta1.Resource.ready:type_name -> apiextensions.fn.proto.v1beta1.Ready
+	1,  // 13: apiextensions.fn.proto.v1beta1.Result.severity:type_name -> apiextensions.fn.proto.v1beta1.Severity
+	7,  // 14: apiextensions.fn.proto.v1beta1.State.ResourcesEntry.value:type_name -> apiextensions.fn.proto.v1beta1.Resource
+	2,  // 15: apiextensions.fn.proto.v1beta1.FunctionRunnerService.RunFunction:input_type -> apiextensions.fn.proto.v1beta1.RunFunctionRequest
+	3,  // 16: apiextensions.fn.proto.v1beta1.FunctionRunnerService.RunFunction:output_type -> apiextensions.fn.proto.v1beta1.RunFunctionResponse
+	16, // [16:17] is the sub-list for method output_type
+	15, // [15:16] is the sub-list for method input_type
+	15, // [15:15] is the sub-list for extension type_name
+	15, // [15:15] is the sub-list for extension extendee
+	0,  // [0:15] is the sub-list for field type_name
 }
 
 func init() { file_apiextensions_fn_proto_v1beta1_run_function_proto_init() }
@@ -798,7 +900,7 @@ func file_apiextensions_fn_proto_v1beta1_run_function_proto_init() {
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_apiextensions_fn_proto_v1beta1_run_function_proto_rawDesc,
-			NumEnums:      1,
+			NumEnums:      2,
 			NumMessages:   9,
 			NumExtensions: 0,
 			NumServices:   1,
diff --git a/apis/apiextensions/fn/proto/v1beta1/run_function.proto b/apis/apiextensions/fn/proto/v1beta1/run_function.proto
index 55588bf54..70f8ae21c 100644
--- a/apis/apiextensions/fn/proto/v1beta1/run_function.proto
+++ b/apis/apiextensions/fn/proto/v1beta1/run_function.proto
@@ -40,9 +40,8 @@ message RunFunctionRequest {
   State observed = 2;
 
   // Desired state according to a Function pipeline. The state passed to a
-  // particular Function may have been accumulated by processing a Composition's
-  // patch-and-transform resources array. It may also have been accumulated by
-  // previous Functions in the pipeline.
+  // particular Function may have been accumulated by previous Functions in the
+  // pipeline.
   State desired = 3;
 
   // Optional input specific to this Function invocation. A JSON representation
@@ -93,13 +92,59 @@ message State {
   map<string, Resource> resources = 2;
 }
 
-// A Resource represents the state of a resource.
+// A Resource represents the state of a composite or composed resource.
 message Resource {
   // The JSON representation of the resource.
+  //
+  // * Crossplane will set this field in a RunFunctionRequest to the entire
+  //   observed state of a resource - including its metadata, spec, and status.
+  //
+  // * A Function should set this field in a RunFunctionRequest to communicate
+  //   the desired state of a composite or composed resource.
+  //
+  // * A Function may only specify the desired status of a composite resource -
+  //   not its metadata or spec. A Function should not return desired metadata
+  //   or spec for a composite resource. This will be ignored.
+  //
+  // * A Function may not specify the desired status of a composed resource -
+  //   only its metadata and spec. A Function should not return desired status
+  //   for a composed resource. This will be ignored.
   google.protobuf.Struct resource = 1;
 
   // The resource's connection details.
+  // 
+  // * Crossplane will set this field in a RunFunctionRequest to communicate the
+  //   the observed connection details of a composite or composed resource.
+  //
+  // * A Function should set this field in a RunFunctionResponse to indicate the
+  //   desired connection details of the composite resource.
+  //
+  // * A Function should not set this field in a RunFunctionResponse to indicate
+  //   the desired connection details of a composed resource. This will be
+  //   ignored.
   map<string, bytes> connection_details = 2;
+
+  // Ready indicates whether the resource should be considered ready.
+  // 
+  // * Crossplane will never set this field in a RunFunctionRequest.
+  //
+  // * A Function should set this field to READY_TRUE in a RunFunctionResponse
+  //   to indicate that a desired composed resource is ready.
+  //
+  // * A Function should not set this field in a RunFunctionResponse to indicate
+  //   that the desired composite resource is ready. This will be ignored.
+  Ready ready = 3;
+}
+
+// Ready indicates whether a composed resource should be considered ready.
+enum Ready {
+  READY_UNSPECIFIED = 0;
+
+  // True means the composed resource has been observed to be ready.
+  READY_TRUE = 1;
+
+  // False means the composed resource has not been observed to be ready.
+  READY_FALSE = 2;
 }
 
 // A Result of running a Function.
diff --git a/apis/apiextensions/v1/composition_common.go b/apis/apiextensions/v1/composition_common.go
index a1f7a0140..4800d6a1d 100644
--- a/apis/apiextensions/v1/composition_common.go
+++ b/apis/apiextensions/v1/composition_common.go
@@ -18,8 +18,6 @@ package v1
 
 import (
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/validation/field"
@@ -37,6 +35,25 @@ import (
 	into composition_revision_types.go.
 */
 
+// A CompositionMode determines what mode of Composition is used
+type CompositionMode string
+
+const (
+	// CompositionModeResources indicates that a Composition uses what is
+	// commonly referred to as "Patch & Transform" or P&T composition. This mode
+	// of Composition uses an array of resources, each a template for a composed
+	// resource.
+	CompositionModeResources CompositionMode = "Resources"
+
+	// CompositionModePipeline indicates that a Composition specifies a pipeline
+	// of Composition Functions, each of which is responsible for producing
+	// composed resources that Crossplane should create or update.
+	//
+	// THIS IS A BETA FEATURE. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
+	CompositionModePipeline CompositionMode = "Pipeline"
+)
+
 // TypeReference is used to refer to a type for declaring compatibility.
 type TypeReference struct {
 	// APIVersion of the type.
@@ -265,154 +282,29 @@ type ConnectionDetail struct {
 	Value *string `json:"value,omitempty"`
 }
 
-// A Function represents a Composition Function.
-type Function struct {
-	// Name of this function. Must be unique within its Composition.
-	Name string `json:"name"`
+// A PipelineStep in a Composition Function pipeline.
+type PipelineStep struct {
+	// Step name. Must be unique within its Pipeline.
+	Step string `json:"step"`
 
-	// Type of this function.
-	// +kubebuilder:validation:Enum=Container
-	Type FunctionType `json:"type"`
+	// FunctionRef is a reference to the Composition Function this step should
+	// execute.
+	FunctionRef FunctionReference `json:"functionRef"`
 
-	// Config is an optional, arbitrary Kubernetes resource (i.e. a resource
+	// Input is an optional, arbitrary Kubernetes resource (i.e. a resource
 	// with an apiVersion and kind) that will be passed to the Composition
-	// Function as the 'config' block of its FunctionIO.
+	// Function as the 'input' of its RunFunctionRequest.
 	// +optional
 	// +kubebuilder:pruning:PreserveUnknownFields
 	// +kubebuilder:validation:EmbeddedResource
-	Config *runtime.RawExtension `json:"config,omitempty"`
-
-	// Container configuration of this function.
-	// +optional
-	Container *ContainerFunction `json:"container,omitempty"`
-}
-
-// Validate this Function.
-func (f *Function) Validate() *field.Error {
-	if f.Type == FunctionTypeContainer {
-		if f.Container == nil {
-			return field.Required(field.NewPath("container"), "cannot be empty for type Container")
-		}
-		return nil
-	}
-	return field.Required(field.NewPath("type"), "the only supported type is Container")
+	Input *runtime.RawExtension `json:"input,omitempty"`
 }
 
-// A FunctionType is a type of Composition Function.
-type FunctionType string
-
-// FunctionType types.
-const (
-	// FunctionTypeContainer represents a Composition Function that is packaged
-	// as an OCI image and run in a container.
-	FunctionTypeContainer FunctionType = "Container"
-)
-
-// NOTE(negz): This is intentionally much more limited than corev1.Container.
-// This is because:
-//
-// * We always expect functions to be short-lived processes.
-// * We never expect functions to listen for incoming requests.
-// * We don't allow functions to mount volumes.
-
-// A ContainerFunction represents an Composition Function that is packaged as an
-// OCI image and run in a container.
-type ContainerFunction struct {
-	// Image specifies the OCI image in which the function is packaged. The
-	// image should include an entrypoint that reads a FunctionIO from stdin and
-	// emits it, optionally mutated, to stdout.
-	Image string `json:"image"`
-
-	// ImagePullPolicy defines the pull policy for the function image.
-	// +optional
-	// +kubebuilder:default=IfNotPresent
-	// +kubebuilder:validation:Enum="IfNotPresent";"Always";"Never"
-	ImagePullPolicy *corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
-
-	// ImagePullSecrets are used to pull images from private OCI registries.
-	// +optional
-	ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
-
-	// Timeout after which the Composition Function will be killed.
-	// +optional
-	// +kubebuilder:default="20s"
-	Timeout *metav1.Duration `json:"timeout,omitempty"`
-
-	// Network configuration for the Composition Function.
-	// +optional
-	Network *ContainerFunctionNetwork `json:"network,omitempty"`
-
-	// Resources that may be used by the Composition Function.
-	// +optional
-	Resources *ContainerFunctionResources `json:"resources,omitempty"`
-
-	// Runner configuration for the Composition Function.
-	// +optional
-	Runner *ContainerFunctionRunner `json:"runner,omitempty"`
-}
-
-// A ContainerFunctionNetworkPolicy specifies the network policy under which
-// a containerized Composition Function will run.
-type ContainerFunctionNetworkPolicy string
-
-const (
-	// ContainerFunctionNetworkPolicyIsolated specifies that the Composition
-	// Function will not have network access; i.e. invoked inside an isolated
-	// network namespace.
-	ContainerFunctionNetworkPolicyIsolated ContainerFunctionNetworkPolicy = "Isolated"
-
-	// ContainerFunctionNetworkPolicyRunner specifies that the Composition
-	// Function will have the same network access as its runner, i.e. share its
-	// runner's network namespace.
-	ContainerFunctionNetworkPolicyRunner ContainerFunctionNetworkPolicy = "Runner"
-)
-
-// ContainerFunctionNetwork represents configuration for a Composition Function.
-type ContainerFunctionNetwork struct {
-	// Policy specifies the network policy under which the Composition Function
-	// will run. Defaults to 'Isolated' - i.e. no network access. Specify
-	// 'Runner' to allow the function the same network access as
-	// its runner.
-	// +optional
-	// +kubebuilder:validation:Enum="Isolated";"Runner"
-	// +kubebuilder:default=Isolated
-	Policy *ContainerFunctionNetworkPolicy `json:"policy,omitempty"`
-}
-
-// ContainerFunctionResources represents compute resources that may be used by a
-// Composition Function.
-type ContainerFunctionResources struct {
-	// Limits specify the maximum compute resources that may be used by the
-	// Composition Function.
-	// +optional
-	Limits *ContainerFunctionResourceLimits `json:"limits,omitempty"`
-
-	// NOTE(negz): We don't presently have any runners that support scheduling,
-	// so we omit Requests for the time being.
-}
-
-// ContainerFunctionResourceLimits specify the maximum compute resources
-// that may be used by a Composition Function.
-type ContainerFunctionResourceLimits struct {
-	// CPU, in cores. (500m = .5 cores)
-	// +kubebuilder:default="100m"
-	// +optional
-	CPU *resource.Quantity `json:"cpu,omitempty"`
-
-	// Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
-	// +kubebuilder:default="128Mi"
-	// +optional
-	Memory *resource.Quantity `json:"memory,omitempty"`
-}
-
-// ContainerFunctionRunner represents runner configuration for a Composition
-// Function.
-type ContainerFunctionRunner struct {
-	// Endpoint specifies how and where Crossplane should reach the runner it
-	// uses to invoke containerized Composition Functions.
-	// +optional
-	// +kubebuilder:default="unix-abstract:crossplane/fn/default.sock"
-	Endpoint *string `json:"endpoint,omitempty"`
+// A FunctionReference references a Composition Function that may be used in a
+// Composition pipeline.
+type FunctionReference struct {
+	// Name of the referenced Function.
+	Name string `json:"name"`
 }
 
 // A StoreConfigReference references a secret store config that may be used to
diff --git a/apis/apiextensions/v1/composition_revision_types.go b/apis/apiextensions/v1/composition_revision_types.go
index 01bf7d515..83fbf46de 100644
--- a/apis/apiextensions/v1/composition_revision_types.go
+++ b/apis/apiextensions/v1/composition_revision_types.go
@@ -41,33 +41,64 @@ type CompositionRevisionSpec struct {
 	// +immutable
 	CompositeTypeRef TypeReference `json:"compositeTypeRef"`
 
-	// PatchSets define a named set of patches that may be included by
-	// any resource in this Composition.
-	// PatchSets cannot themselves refer to other PatchSets.
+	// Mode controls what type or "mode" of Composition will be used.
+	//
+	// "Resources" (the default) indicates that a Composition uses what is
+	// commonly referred to as "Patch & Transform" or P&T composition. This mode
+	// of Composition uses an array of resources, each a template for a composed
+	// resource.
+	//
+	// "Pipeline" indicates that a Composition specifies a pipeline
+	// of Composition Functions, each of which is responsible for producing
+	// composed resources that Crossplane should create or update. THE PIPELINE
+	// MODE IS A BETA FEATURE. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
+	// +optional
+	// +kubebuilder:validation:Enum=Resources;Pipeline
+	// +kubebuilder:default=Resources
+	Mode *CompositionMode `json:"mode,omitempty"`
+
+	// PatchSets define a named set of patches that may be included by any
+	// resource in this Composition. PatchSets cannot themselves refer to other
+	// PatchSets.
+	//
+	// PatchSets are only used by the "Resources" mode of Composition. They
+	// are ignored by other modes.
 	// +optional
 	PatchSets []PatchSet `json:"patchSets,omitempty"`
 
 	// Environment configures the environment in which resources are rendered.
+	//
+	// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored
+	// unless the relevant Crossplane feature flag is enabled, and may be
+	// changed or removed without notice.
 	// +optional
 	Environment *EnvironmentConfiguration `json:"environment,omitempty"`
 
-	// Resources is the list of resource templates that will be used when a
+	// Resources is a list of resource templates that will be used when a
 	// composite resource referring to this composition is created.
+	//
+	// Resources are only used by the "Resources" mode of Composition. They are
+	// ignored by other modes.
 	// +optional
-	Resources []ComposedTemplate `json:"resources"`
-
-	// Functions is list of Composition Functions that will be used when a
-	// composite resource referring to this composition is created. At least one
-	// of resources and functions must be specified. If both are specified the
-	// resources will be rendered first, then passed to the functions for
-	// further processing.
+	Resources []ComposedTemplate `json:"resources,omitempty"`
+
+	// Pipeline is a list of composition function steps that will be used when a
+	// composite resource referring to this composition is created. One of
+	// resources and pipeline must be specified - you cannot specify both.
+	//
+	// The Pipeline is only used by the "Pipeline" mode of Composition. It is
+	// ignored by other modes.
+	//
+	// THIS IS A BETA FIELD. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
 	// +optional
-	Functions []Function `json:"functions,omitempty"`
+	Pipeline []PipelineStep `json:"pipeline,omitempty"`
 
 	// WriteConnectionSecretsToNamespace specifies the namespace in which the
 	// connection secrets of composite resource dynamically provisioned using
 	// this composition will be created.
-	// This field is planned to be removed in a future release in favor of
+	// This field is planned to be replaced in a future release in favor of
 	// PublishConnectionDetailsWithStoreConfigRef. Currently, both could be
 	// set independently and connection details would be published to both
 	// without affecting each other as long as related fields at MR level
@@ -78,6 +109,10 @@ type CompositionRevisionSpec struct {
 	// PublishConnectionDetailsWithStoreConfig specifies the secret store config
 	// with which the connection details of composite resources dynamically
 	// provisioned using this composition will be published.
+	//
+	// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored
+	// unless the relevant Crossplane feature flag is enabled, and may be
+	// changed or removed without notice.
 	// +optional
 	// +kubebuilder:default={"name": "default"}
 	PublishConnectionDetailsWithStoreConfigRef *StoreConfigReference `json:"publishConnectionDetailsWithStoreConfigRef,omitempty"`
diff --git a/apis/apiextensions/v1/composition_types.go b/apis/apiextensions/v1/composition_types.go
index d2247210d..597f12090 100644
--- a/apis/apiextensions/v1/composition_types.go
+++ b/apis/apiextensions/v1/composition_types.go
@@ -27,13 +27,34 @@ type CompositionSpec struct {
 	// +immutable
 	CompositeTypeRef TypeReference `json:"compositeTypeRef"`
 
-	// PatchSets define a named set of patches that may be included by
-	// any resource in this Composition.
-	// PatchSets cannot themselves refer to other PatchSets.
+	// Mode controls what type or "mode" of Composition will be used.
+	//
+	// "Resources" (the default) indicates that a Composition uses what is
+	// commonly referred to as "Patch & Transform" or P&T composition. This mode
+	// of Composition uses an array of resources, each a template for a composed
+	// resource.
+	//
+	// "Pipeline" indicates that a Composition specifies a pipeline
+	// of Composition Functions, each of which is responsible for producing
+	// composed resources that Crossplane should create or update. THE PIPELINE
+	// MODE IS A BETA FEATURE. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
+	// +optional
+	// +kubebuilder:validation:Enum=Resources;Pipeline
+	// +kubebuilder:default=Resources
+	Mode *CompositionMode `json:"mode,omitempty"`
+
+	// PatchSets define a named set of patches that may be included by any
+	// resource in this Composition. PatchSets cannot themselves refer to other
+	// PatchSets.
+	//
+	// PatchSets are only used by the "Resources" mode of Composition. They
+	// are ignored by other modes.
 	// +optional
 	PatchSets []PatchSet `json:"patchSets,omitempty"`
 
 	// Environment configures the environment in which resources are rendered.
+	//
 	// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored
 	// unless the relevant Crossplane feature flag is enabled, and may be
 	// changed or removed without notice.
@@ -41,24 +62,24 @@ type CompositionSpec struct {
 	Environment *EnvironmentConfiguration `json:"environment,omitempty"`
 
 	// Resources is a list of resource templates that will be used when a
-	// composite resource referring to this composition is created. At least one
-	// of resources and functions must be specififed. If both are specified the
-	// resources will be rendered first, then passed to the functions for
-	// further processing.
+	// composite resource referring to this composition is created.
+	//
+	// Resources are only used by the "Resources" mode of Composition. They are
+	// ignored by other modes.
 	// +optional
 	Resources []ComposedTemplate `json:"resources,omitempty"`
 
-	// Functions is list of Composition Functions that will be used when a
-	// composite resource referring to this composition is created. At least one
-	// of resources and functions must be specified. If both are specified the
-	// resources will be rendered first, then passed to the functions for
-	// further processing.
+	// Pipeline is a list of composition function steps that will be used when a
+	// composite resource referring to this composition is created. One of
+	// resources and pipeline must be specified - you cannot specify both.
 	//
-	// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored
-	// unless the relevant Crossplane feature flag is enabled, and may be
-	// changed or removed without notice.
+	// The Pipeline is only used by the "Pipeline" mode of Composition. It is
+	// ignored by other modes.
+	//
+	// THIS IS A BETA FIELD. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
 	// +optional
-	Functions []Function `json:"functions,omitempty"`
+	Pipeline []PipelineStep `json:"pipeline,omitempty"`
 
 	// WriteConnectionSecretsToNamespace specifies the namespace in which the
 	// connection secrets of composite resource dynamically provisioned using
diff --git a/apis/apiextensions/v1/composition_validation.go b/apis/apiextensions/v1/composition_validation.go
index 738e6e11b..75b8586bb 100644
--- a/apis/apiextensions/v1/composition_validation.go
+++ b/apis/apiextensions/v1/composition_validation.go
@@ -28,9 +28,10 @@ import (
 func (c *Composition) Validate() (warns []string, errs field.ErrorList) {
 	type validationFunc func() field.ErrorList
 	validations := []validationFunc{
+		c.validateMode,
 		c.validatePatchSets,
 		c.validateResources,
-		c.validateFunctions,
+		c.validatePipeline,
 		c.validateEnvironment,
 	}
 	for _, f := range validations {
@@ -39,16 +40,35 @@ func (c *Composition) Validate() (warns []string, errs field.ErrorList) {
 	return nil, errs
 }
 
-func (c *Composition) validateFunctions() (errs field.ErrorList) {
-	seen := map[string]bool{}
-	for i, f := range c.Spec.Functions {
-		if seen[f.Name] {
-			errs = append(errs, field.Duplicate(field.NewPath("spec", "functions").Index(i).Child("name"), f.Name))
+func (c *Composition) validateMode() (errs field.ErrorList) {
+	// "Resources" mode was the original mode. It predates the mode field, so
+	// it's the default if mode isn't specified.
+	m := CompositionModeResources
+	if c.Spec.Mode != nil {
+		m = *c.Spec.Mode
+	}
+
+	switch m {
+	case CompositionModeResources:
+		if len(c.Spec.Resources) == 0 {
+			errs = append(errs, field.Required(field.NewPath("spec", "resources"), "an array of resources is required in Resources mode (the default if no mode is specified)"))
 		}
-		seen[f.Name] = true
-		if err := f.Validate(); err != nil {
-			errs = append(errs, verrors.WrapFieldError(err, field.NewPath("spec", "functions").Index(i)))
+	case CompositionModePipeline:
+		if len(c.Spec.Pipeline) == 0 {
+			errs = append(errs, field.Required(field.NewPath("spec", "pipeline"), "an array of pipeline steps is required in Pipeline mode"))
+		}
+	}
+
+	return errs
+}
+
+func (c *Composition) validatePipeline() (errs field.ErrorList) {
+	seen := map[string]bool{}
+	for i, f := range c.Spec.Pipeline {
+		if seen[f.Step] {
+			errs = append(errs, field.Duplicate(field.NewPath("spec", "pipeline").Index(i).Child("step"), f.Step))
 		}
+		seen[f.Step] = true
 	}
 	return errs
 }
@@ -118,8 +138,8 @@ func (c *Composition) validateResources() (errs field.ErrorList) {
 //     template N.
 //  2. All resources have unique names: because other parts of the code require so.
 //  3. If the composition has any functions, it must have only named resources: This is necessary for the
-//     FunctionComposer to be able to associate entries in the spec.resources array with entries in a FunctionIO's observed
-//     and desired arrays
+//     FunctionComposer to be able to associate entries in the spec.resources array with entries in a RunFunctionRequest's observed
+//     and desired objects.
 func (c *Composition) validateResourceNames() (errs field.ErrorList) {
 	seen := map[string]bool{}
 	for resourceIndex, res := range c.Spec.Resources {
@@ -128,7 +148,7 @@ func (c *Composition) validateResourceNames() (errs field.ErrorList) {
 		name := res.GetName()
 		if name == "" {
 			// If the composition has any functions, it must have only named resources.
-			if len(c.Spec.Functions) != 0 {
+			if len(c.Spec.Pipeline) != 0 {
 				errs = append(errs, field.Required(field.NewPath("spec", "resources").Index(resourceIndex).Child("name"), "cannot have anonymous resources when composition has functions"))
 				continue
 			}
diff --git a/apis/apiextensions/v1/zz_generated.conversion.go b/apis/apiextensions/v1/zz_generated.conversion.go
index 101a3fbdf..5c8f29753 100755
--- a/apis/apiextensions/v1/zz_generated.conversion.go
+++ b/apis/apiextensions/v1/zz_generated.conversion.go
@@ -3,12 +3,10 @@
 package v1
 
 import (
-	v13 "github.com/crossplane/crossplane-runtime/apis/common/v1"
-	v1 "k8s.io/api/core/v1"
-	v12 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	v11 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v11 "github.com/crossplane/crossplane-runtime/apis/common/v1"
+	v12 "k8s.io/api/core/v1"
+	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
-	"time"
 )
 
 type GeneratedRevisionSpecConverter struct{}
@@ -16,6 +14,12 @@ type GeneratedRevisionSpecConverter struct{}
 func (c *GeneratedRevisionSpecConverter) FromRevisionSpec(source CompositionRevisionSpec) CompositionSpec {
 	var v1CompositionSpec CompositionSpec
 	v1CompositionSpec.CompositeTypeRef = c.v1TypeReferenceToV1TypeReference(source.CompositeTypeRef)
+	var pV1CompositionMode *CompositionMode
+	if source.Mode != nil {
+		v1CompositionMode := CompositionMode(*source.Mode)
+		pV1CompositionMode = &v1CompositionMode
+	}
+	v1CompositionSpec.Mode = pV1CompositionMode
 	var v1PatchSetList []PatchSet
 	if source.PatchSets != nil {
 		v1PatchSetList = make([]PatchSet, len(source.PatchSets))
@@ -33,14 +37,14 @@ func (c *GeneratedRevisionSpecConverter) FromRevisionSpec(source CompositionRevi
 		}
 	}
 	v1CompositionSpec.Resources = v1ComposedTemplateList
-	var v1FunctionList []Function
-	if source.Functions != nil {
-		v1FunctionList = make([]Function, len(source.Functions))
-		for k := 0; k < len(source.Functions); k++ {
-			v1FunctionList[k] = c.v1FunctionToV1Function(source.Functions[k])
+	var v1PipelineStepList []PipelineStep
+	if source.Pipeline != nil {
+		v1PipelineStepList = make([]PipelineStep, len(source.Pipeline))
+		for k := 0; k < len(source.Pipeline); k++ {
+			v1PipelineStepList[k] = c.v1PipelineStepToV1PipelineStep(source.Pipeline[k])
 		}
 	}
-	v1CompositionSpec.Functions = v1FunctionList
+	v1CompositionSpec.Pipeline = v1PipelineStepList
 	var pString *string
 	if source.WriteConnectionSecretsToNamespace != nil {
 		xstring := *source.WriteConnectionSecretsToNamespace
@@ -53,6 +57,12 @@ func (c *GeneratedRevisionSpecConverter) FromRevisionSpec(source CompositionRevi
 func (c *GeneratedRevisionSpecConverter) ToRevisionSpec(source CompositionSpec) CompositionRevisionSpec {
 	var v1CompositionRevisionSpec CompositionRevisionSpec
 	v1CompositionRevisionSpec.CompositeTypeRef = c.v1TypeReferenceToV1TypeReference(source.CompositeTypeRef)
+	var pV1CompositionMode *CompositionMode
+	if source.Mode != nil {
+		v1CompositionMode := CompositionMode(*source.Mode)
+		pV1CompositionMode = &v1CompositionMode
+	}
+	v1CompositionRevisionSpec.Mode = pV1CompositionMode
 	var v1PatchSetList []PatchSet
 	if source.PatchSets != nil {
 		v1PatchSetList = make([]PatchSet, len(source.PatchSets))
@@ -70,14 +80,14 @@ func (c *GeneratedRevisionSpecConverter) ToRevisionSpec(source CompositionSpec)
 		}
 	}
 	v1CompositionRevisionSpec.Resources = v1ComposedTemplateList
-	var v1FunctionList []Function
-	if source.Functions != nil {
-		v1FunctionList = make([]Function, len(source.Functions))
-		for k := 0; k < len(source.Functions); k++ {
-			v1FunctionList[k] = c.v1FunctionToV1Function(source.Functions[k])
+	var v1PipelineStepList []PipelineStep
+	if source.Pipeline != nil {
+		v1PipelineStepList = make([]PipelineStep, len(source.Pipeline))
+		for k := 0; k < len(source.Pipeline); k++ {
+			v1PipelineStepList[k] = c.v1PipelineStepToV1PipelineStep(source.Pipeline[k])
 		}
 	}
-	v1CompositionRevisionSpec.Functions = v1FunctionList
+	v1CompositionRevisionSpec.Pipeline = v1PipelineStepList
 	var pString *string
 	if source.WriteConnectionSecretsToNamespace != nil {
 		xstring := *source.WriteConnectionSecretsToNamespace
@@ -113,80 +123,6 @@ func (c *GeneratedRevisionSpecConverter) pV1CombineToPV1Combine(source *Combine)
 	}
 	return pV1Combine
 }
-func (c *GeneratedRevisionSpecConverter) pV1ContainerFunctionNetworkToPV1ContainerFunctionNetwork(source *ContainerFunctionNetwork) *ContainerFunctionNetwork {
-	var pV1ContainerFunctionNetwork *ContainerFunctionNetwork
-	if source != nil {
-		var v1ContainerFunctionNetwork ContainerFunctionNetwork
-		var pV1ContainerFunctionNetworkPolicy *ContainerFunctionNetworkPolicy
-		if (*source).Policy != nil {
-			v1ContainerFunctionNetworkPolicy := ContainerFunctionNetworkPolicy(*(*source).Policy)
-			pV1ContainerFunctionNetworkPolicy = &v1ContainerFunctionNetworkPolicy
-		}
-		v1ContainerFunctionNetwork.Policy = pV1ContainerFunctionNetworkPolicy
-		pV1ContainerFunctionNetwork = &v1ContainerFunctionNetwork
-	}
-	return pV1ContainerFunctionNetwork
-}
-func (c *GeneratedRevisionSpecConverter) pV1ContainerFunctionResourceLimitsToPV1ContainerFunctionResourceLimits(source *ContainerFunctionResourceLimits) *ContainerFunctionResourceLimits {
-	var pV1ContainerFunctionResourceLimits *ContainerFunctionResourceLimits
-	if source != nil {
-		var v1ContainerFunctionResourceLimits ContainerFunctionResourceLimits
-		v1ContainerFunctionResourceLimits.CPU = ConvertResourceQuantity((*source).CPU)
-		v1ContainerFunctionResourceLimits.Memory = ConvertResourceQuantity((*source).Memory)
-		pV1ContainerFunctionResourceLimits = &v1ContainerFunctionResourceLimits
-	}
-	return pV1ContainerFunctionResourceLimits
-}
-func (c *GeneratedRevisionSpecConverter) pV1ContainerFunctionResourcesToPV1ContainerFunctionResources(source *ContainerFunctionResources) *ContainerFunctionResources {
-	var pV1ContainerFunctionResources *ContainerFunctionResources
-	if source != nil {
-		var v1ContainerFunctionResources ContainerFunctionResources
-		v1ContainerFunctionResources.Limits = c.pV1ContainerFunctionResourceLimitsToPV1ContainerFunctionResourceLimits((*source).Limits)
-		pV1ContainerFunctionResources = &v1ContainerFunctionResources
-	}
-	return pV1ContainerFunctionResources
-}
-func (c *GeneratedRevisionSpecConverter) pV1ContainerFunctionRunnerToPV1ContainerFunctionRunner(source *ContainerFunctionRunner) *ContainerFunctionRunner {
-	var pV1ContainerFunctionRunner *ContainerFunctionRunner
-	if source != nil {
-		var v1ContainerFunctionRunner ContainerFunctionRunner
-		var pString *string
-		if (*source).Endpoint != nil {
-			xstring := *(*source).Endpoint
-			pString = &xstring
-		}
-		v1ContainerFunctionRunner.Endpoint = pString
-		pV1ContainerFunctionRunner = &v1ContainerFunctionRunner
-	}
-	return pV1ContainerFunctionRunner
-}
-func (c *GeneratedRevisionSpecConverter) pV1ContainerFunctionToPV1ContainerFunction(source *ContainerFunction) *ContainerFunction {
-	var pV1ContainerFunction *ContainerFunction
-	if source != nil {
-		var v1ContainerFunction ContainerFunction
-		v1ContainerFunction.Image = (*source).Image
-		var pV1PullPolicy *v1.PullPolicy
-		if (*source).ImagePullPolicy != nil {
-			v1PullPolicy := v1.PullPolicy(*(*source).ImagePullPolicy)
-			pV1PullPolicy = &v1PullPolicy
-		}
-		v1ContainerFunction.ImagePullPolicy = pV1PullPolicy
-		var v1LocalObjectReferenceList []v1.LocalObjectReference
-		if (*source).ImagePullSecrets != nil {
-			v1LocalObjectReferenceList = make([]v1.LocalObjectReference, len((*source).ImagePullSecrets))
-			for i := 0; i < len((*source).ImagePullSecrets); i++ {
-				v1LocalObjectReferenceList[i] = c.v1LocalObjectReferenceToV1LocalObjectReference((*source).ImagePullSecrets[i])
-			}
-		}
-		v1ContainerFunction.ImagePullSecrets = v1LocalObjectReferenceList
-		v1ContainerFunction.Timeout = c.pV1DurationToPV1Duration((*source).Timeout)
-		v1ContainerFunction.Network = c.pV1ContainerFunctionNetworkToPV1ContainerFunctionNetwork((*source).Network)
-		v1ContainerFunction.Resources = c.pV1ContainerFunctionResourcesToPV1ContainerFunctionResources((*source).Resources)
-		v1ContainerFunction.Runner = c.pV1ContainerFunctionRunnerToPV1ContainerFunctionRunner((*source).Runner)
-		pV1ContainerFunction = &v1ContainerFunction
-	}
-	return pV1ContainerFunction
-}
 func (c *GeneratedRevisionSpecConverter) pV1ConvertTransformToPV1ConvertTransform(source *ConvertTransform) *ConvertTransform {
 	var pV1ConvertTransform *ConvertTransform
 	if source != nil {
@@ -202,20 +138,11 @@ func (c *GeneratedRevisionSpecConverter) pV1ConvertTransformToPV1ConvertTransfor
 	}
 	return pV1ConvertTransform
 }
-func (c *GeneratedRevisionSpecConverter) pV1DurationToPV1Duration(source *v11.Duration) *v11.Duration {
-	var pV1Duration *v11.Duration
-	if source != nil {
-		var v1Duration v11.Duration
-		v1Duration.Duration = time.Duration((*source).Duration)
-		pV1Duration = &v1Duration
-	}
-	return pV1Duration
-}
 func (c *GeneratedRevisionSpecConverter) pV1EnvironmentConfigurationToPV1EnvironmentConfiguration(source *EnvironmentConfiguration) *EnvironmentConfiguration {
 	var pV1EnvironmentConfiguration *EnvironmentConfiguration
 	if source != nil {
 		var v1EnvironmentConfiguration EnvironmentConfiguration
-		mapStringV1JSON := make(map[string]v12.JSON, len((*source).DefaultData))
+		mapStringV1JSON := make(map[string]v1.JSON, len((*source).DefaultData))
 		for key, value := range (*source).DefaultData {
 			mapStringV1JSON[key] = c.v1JSONToV1JSON(value)
 		}
@@ -278,7 +205,7 @@ func (c *GeneratedRevisionSpecConverter) pV1MapTransformToPV1MapTransform(source
 	var pV1MapTransform *MapTransform
 	if source != nil {
 		var v1MapTransform MapTransform
-		mapStringV1JSON := make(map[string]v12.JSON, len((*source).Pairs))
+		mapStringV1JSON := make(map[string]v1.JSON, len((*source).Pairs))
 		for key, value := range (*source).Pairs {
 			mapStringV1JSON[key] = c.v1JSONToV1JSON(value)
 		}
@@ -291,8 +218,8 @@ func (c *GeneratedRevisionSpecConverter) pV1MatchConditionReadinessCheckToPV1Mat
 	var pV1MatchConditionReadinessCheck *MatchConditionReadinessCheck
 	if source != nil {
 		var v1MatchConditionReadinessCheck MatchConditionReadinessCheck
-		v1MatchConditionReadinessCheck.Type = v13.ConditionType((*source).Type)
-		v1MatchConditionReadinessCheck.Status = v1.ConditionStatus((*source).Status)
+		v1MatchConditionReadinessCheck.Type = v11.ConditionType((*source).Type)
+		v1MatchConditionReadinessCheck.Status = v12.ConditionStatus((*source).Status)
 		pV1MatchConditionReadinessCheck = &v1MatchConditionReadinessCheck
 	}
 	return pV1MatchConditionReadinessCheck
@@ -342,10 +269,10 @@ func (c *GeneratedRevisionSpecConverter) pV1MathTransformToPV1MathTransform(sour
 	}
 	return pV1MathTransform
 }
-func (c *GeneratedRevisionSpecConverter) pV1MergeOptionsToPV1MergeOptions(source *v13.MergeOptions) *v13.MergeOptions {
-	var pV1MergeOptions *v13.MergeOptions
+func (c *GeneratedRevisionSpecConverter) pV1MergeOptionsToPV1MergeOptions(source *v11.MergeOptions) *v11.MergeOptions {
+	var pV1MergeOptions *v11.MergeOptions
 	if source != nil {
-		var v1MergeOptions v13.MergeOptions
+		var v1MergeOptions v11.MergeOptions
 		var pBool *bool
 		if (*source).KeepMapValues != nil {
 			xbool := *(*source).KeepMapValues
@@ -377,19 +304,19 @@ func (c *GeneratedRevisionSpecConverter) pV1PatchPolicyToPV1PatchPolicy(source *
 	}
 	return pV1PatchPolicy
 }
-func (c *GeneratedRevisionSpecConverter) pV1PolicyToPV1Policy(source *v13.Policy) *v13.Policy {
-	var pV1Policy *v13.Policy
+func (c *GeneratedRevisionSpecConverter) pV1PolicyToPV1Policy(source *v11.Policy) *v11.Policy {
+	var pV1Policy *v11.Policy
 	if source != nil {
-		var v1Policy v13.Policy
-		var pV1ResolvePolicy *v13.ResolvePolicy
+		var v1Policy v11.Policy
+		var pV1ResolvePolicy *v11.ResolvePolicy
 		if (*source).Resolve != nil {
-			v1ResolvePolicy := v13.ResolvePolicy(*(*source).Resolve)
+			v1ResolvePolicy := v11.ResolvePolicy(*(*source).Resolve)
 			pV1ResolvePolicy = &v1ResolvePolicy
 		}
 		v1Policy.Resolve = pV1ResolvePolicy
-		var pV1ResolutionPolicy *v13.ResolutionPolicy
+		var pV1ResolutionPolicy *v11.ResolutionPolicy
 		if (*source).Resolution != nil {
-			v1ResolutionPolicy := v13.ResolutionPolicy(*(*source).Resolution)
+			v1ResolutionPolicy := v11.ResolutionPolicy(*(*source).Resolution)
 			pV1ResolutionPolicy = &v1ResolutionPolicy
 		}
 		v1Policy.Resolution = pV1ResolutionPolicy
@@ -590,16 +517,13 @@ func (c *GeneratedRevisionSpecConverter) v1EnvironmentSourceToV1EnvironmentSourc
 	v1EnvironmentSource.Selector = c.pV1EnvironmentSourceSelectorToPV1EnvironmentSourceSelector(source.Selector)
 	return v1EnvironmentSource
 }
-func (c *GeneratedRevisionSpecConverter) v1FunctionToV1Function(source Function) Function {
-	var v1Function Function
-	v1Function.Name = source.Name
-	v1Function.Type = FunctionType(source.Type)
-	v1Function.Config = c.pRuntimeRawExtensionToPRuntimeRawExtension(source.Config)
-	v1Function.Container = c.pV1ContainerFunctionToPV1ContainerFunction(source.Container)
-	return v1Function
+func (c *GeneratedRevisionSpecConverter) v1FunctionReferenceToV1FunctionReference(source FunctionReference) FunctionReference {
+	var v1FunctionReference FunctionReference
+	v1FunctionReference.Name = source.Name
+	return v1FunctionReference
 }
-func (c *GeneratedRevisionSpecConverter) v1JSONToV1JSON(source v12.JSON) v12.JSON {
-	var v1JSON v12.JSON
+func (c *GeneratedRevisionSpecConverter) v1JSONToV1JSON(source v1.JSON) v1.JSON {
+	var v1JSON v1.JSON
 	var byteList []uint8
 	if source.Raw != nil {
 		byteList = make([]uint8, len(source.Raw))
@@ -610,11 +534,6 @@ func (c *GeneratedRevisionSpecConverter) v1JSONToV1JSON(source v12.JSON) v12.JSO
 	v1JSON.Raw = byteList
 	return v1JSON
 }
-func (c *GeneratedRevisionSpecConverter) v1LocalObjectReferenceToV1LocalObjectReference(source v1.LocalObjectReference) v1.LocalObjectReference {
-	var v1LocalObjectReference v1.LocalObjectReference
-	v1LocalObjectReference.Name = source.Name
-	return v1LocalObjectReference
-}
 func (c *GeneratedRevisionSpecConverter) v1MatchTransformPatternToV1MatchTransformPattern(source MatchTransformPattern) MatchTransformPattern {
 	var v1MatchTransformPattern MatchTransformPattern
 	v1MatchTransformPattern.Type = MatchTransformPatternType(source.Type)
@@ -679,6 +598,13 @@ func (c *GeneratedRevisionSpecConverter) v1PatchToV1Patch(source Patch) Patch {
 	v1Patch.Policy = c.pV1PatchPolicyToPV1PatchPolicy(source.Policy)
 	return v1Patch
 }
+func (c *GeneratedRevisionSpecConverter) v1PipelineStepToV1PipelineStep(source PipelineStep) PipelineStep {
+	var v1PipelineStep PipelineStep
+	v1PipelineStep.Step = source.Step
+	v1PipelineStep.FunctionRef = c.v1FunctionReferenceToV1FunctionReference(source.FunctionRef)
+	v1PipelineStep.Input = c.pRuntimeRawExtensionToPRuntimeRawExtension(source.Input)
+	return v1PipelineStep
+}
 func (c *GeneratedRevisionSpecConverter) v1ReadinessCheckToV1ReadinessCheck(source ReadinessCheck) ReadinessCheck {
 	var v1ReadinessCheck ReadinessCheck
 	v1ReadinessCheck.Type = ReadinessCheckType(source.Type)
diff --git a/apis/apiextensions/v1/zz_generated.deepcopy.go b/apis/apiextensions/v1/zz_generated.deepcopy.go
index 439b63c0e..6d58fb077 100644
--- a/apis/apiextensions/v1/zz_generated.deepcopy.go
+++ b/apis/apiextensions/v1/zz_generated.deepcopy.go
@@ -22,9 +22,7 @@ package v1
 
 import (
 	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
-	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -482,6 +480,11 @@ func (in *CompositionRevisionList) DeepCopyObject() runtime.Object {
 func (in *CompositionRevisionSpec) DeepCopyInto(out *CompositionRevisionSpec) {
 	*out = *in
 	out.CompositeTypeRef = in.CompositeTypeRef
+	if in.Mode != nil {
+		in, out := &in.Mode, &out.Mode
+		*out = new(CompositionMode)
+		**out = **in
+	}
 	if in.PatchSets != nil {
 		in, out := &in.PatchSets, &out.PatchSets
 		*out = make([]PatchSet, len(*in))
@@ -501,9 +504,9 @@ func (in *CompositionRevisionSpec) DeepCopyInto(out *CompositionRevisionSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
-	if in.Functions != nil {
-		in, out := &in.Functions, &out.Functions
-		*out = make([]Function, len(*in))
+	if in.Pipeline != nil {
+		in, out := &in.Pipeline, &out.Pipeline
+		*out = make([]PipelineStep, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -550,6 +553,11 @@ func (in *CompositionRevisionStatus) DeepCopy() *CompositionRevisionStatus {
 func (in *CompositionSpec) DeepCopyInto(out *CompositionSpec) {
 	*out = *in
 	out.CompositeTypeRef = in.CompositeTypeRef
+	if in.Mode != nil {
+		in, out := &in.Mode, &out.Mode
+		*out = new(CompositionMode)
+		**out = **in
+	}
 	if in.PatchSets != nil {
 		in, out := &in.PatchSets, &out.PatchSets
 		*out = make([]PatchSet, len(*in))
@@ -569,9 +577,9 @@ func (in *CompositionSpec) DeepCopyInto(out *CompositionSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
-	if in.Functions != nil {
-		in, out := &in.Functions, &out.Functions
-		*out = make([]Function, len(*in))
+	if in.Pipeline != nil {
+		in, out := &in.Pipeline, &out.Pipeline
+		*out = make([]PipelineStep, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -638,136 +646,6 @@ func (in *ConnectionDetail) DeepCopy() *ConnectionDetail {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunction) DeepCopyInto(out *ContainerFunction) {
-	*out = *in
-	if in.ImagePullPolicy != nil {
-		in, out := &in.ImagePullPolicy, &out.ImagePullPolicy
-		*out = new(corev1.PullPolicy)
-		**out = **in
-	}
-	if in.ImagePullSecrets != nil {
-		in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
-		*out = make([]corev1.LocalObjectReference, len(*in))
-		copy(*out, *in)
-	}
-	if in.Timeout != nil {
-		in, out := &in.Timeout, &out.Timeout
-		*out = new(metav1.Duration)
-		**out = **in
-	}
-	if in.Network != nil {
-		in, out := &in.Network, &out.Network
-		*out = new(ContainerFunctionNetwork)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.Resources != nil {
-		in, out := &in.Resources, &out.Resources
-		*out = new(ContainerFunctionResources)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.Runner != nil {
-		in, out := &in.Runner, &out.Runner
-		*out = new(ContainerFunctionRunner)
-		(*in).DeepCopyInto(*out)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunction.
-func (in *ContainerFunction) DeepCopy() *ContainerFunction {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunction)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionNetwork) DeepCopyInto(out *ContainerFunctionNetwork) {
-	*out = *in
-	if in.Policy != nil {
-		in, out := &in.Policy, &out.Policy
-		*out = new(ContainerFunctionNetworkPolicy)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionNetwork.
-func (in *ContainerFunctionNetwork) DeepCopy() *ContainerFunctionNetwork {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionNetwork)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionResourceLimits) DeepCopyInto(out *ContainerFunctionResourceLimits) {
-	*out = *in
-	if in.CPU != nil {
-		in, out := &in.CPU, &out.CPU
-		x := (*in).DeepCopy()
-		*out = &x
-	}
-	if in.Memory != nil {
-		in, out := &in.Memory, &out.Memory
-		x := (*in).DeepCopy()
-		*out = &x
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionResourceLimits.
-func (in *ContainerFunctionResourceLimits) DeepCopy() *ContainerFunctionResourceLimits {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionResourceLimits)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionResources) DeepCopyInto(out *ContainerFunctionResources) {
-	*out = *in
-	if in.Limits != nil {
-		in, out := &in.Limits, &out.Limits
-		*out = new(ContainerFunctionResourceLimits)
-		(*in).DeepCopyInto(*out)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionResources.
-func (in *ContainerFunctionResources) DeepCopy() *ContainerFunctionResources {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionResources)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionRunner) DeepCopyInto(out *ContainerFunctionRunner) {
-	*out = *in
-	if in.Endpoint != nil {
-		in, out := &in.Endpoint, &out.Endpoint
-		*out = new(string)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionRunner.
-func (in *ContainerFunctionRunner) DeepCopy() *ContainerFunctionRunner {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionRunner)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ConvertTransform) DeepCopyInto(out *ConvertTransform) {
 	*out = *in
@@ -969,26 +847,16 @@ func (in *EnvironmentSourceSelectorLabelMatcher) DeepCopy() *EnvironmentSourceSe
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Function) DeepCopyInto(out *Function) {
+func (in *FunctionReference) DeepCopyInto(out *FunctionReference) {
 	*out = *in
-	if in.Config != nil {
-		in, out := &in.Config, &out.Config
-		*out = new(runtime.RawExtension)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.Container != nil {
-		in, out := &in.Container, &out.Container
-		*out = new(ContainerFunction)
-		(*in).DeepCopyInto(*out)
-	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Function.
-func (in *Function) DeepCopy() *Function {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionReference.
+func (in *FunctionReference) DeepCopy() *FunctionReference {
 	if in == nil {
 		return nil
 	}
-	out := new(Function)
+	out := new(FunctionReference)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -1218,6 +1086,27 @@ func (in *PatchSet) DeepCopy() *PatchSet {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PipelineStep) DeepCopyInto(out *PipelineStep) {
+	*out = *in
+	out.FunctionRef = in.FunctionRef
+	if in.Input != nil {
+		in, out := &in.Input, &out.Input
+		*out = new(runtime.RawExtension)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelineStep.
+func (in *PipelineStep) DeepCopy() *PipelineStep {
+	if in == nil {
+		return nil
+	}
+	out := new(PipelineStep)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ReadinessCheck) DeepCopyInto(out *ReadinessCheck) {
 	*out = *in
diff --git a/apis/apiextensions/v1beta1/zz_generated.composition_common.go b/apis/apiextensions/v1beta1/zz_generated.composition_common.go
index 564db5b83..d2b488430 100644
--- a/apis/apiextensions/v1beta1/zz_generated.composition_common.go
+++ b/apis/apiextensions/v1beta1/zz_generated.composition_common.go
@@ -20,8 +20,6 @@ package v1beta1
 
 import (
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/validation/field"
@@ -39,6 +37,25 @@ import (
 	into composition_revision_types.go.
 */
 
+// A CompositionMode determines what mode of Composition is used
+type CompositionMode string
+
+const (
+	// CompositionModeResources indicates that a Composition uses what is
+	// commonly referred to as "Patch & Transform" or P&T composition. This mode
+	// of Composition uses an array of resources, each a template for a composed
+	// resource.
+	CompositionModeResources CompositionMode = "Resources"
+
+	// CompositionModePipeline indicates that a Composition specifies a pipeline
+	// of Composition Functions, each of which is responsible for producing
+	// composed resources that Crossplane should create or update.
+	//
+	// THIS IS A BETA FEATURE. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
+	CompositionModePipeline CompositionMode = "Pipeline"
+)
+
 // TypeReference is used to refer to a type for declaring compatibility.
 type TypeReference struct {
 	// APIVersion of the type.
@@ -267,154 +284,29 @@ type ConnectionDetail struct {
 	Value *string `json:"value,omitempty"`
 }
 
-// A Function represents a Composition Function.
-type Function struct {
-	// Name of this function. Must be unique within its Composition.
-	Name string `json:"name"`
+// A PipelineStep in a Composition Function pipeline.
+type PipelineStep struct {
+	// Step name. Must be unique within its Pipeline.
+	Step string `json:"step"`
 
-	// Type of this function.
-	// +kubebuilder:validation:Enum=Container
-	Type FunctionType `json:"type"`
+	// FunctionRef is a reference to the Composition Function this step should
+	// execute.
+	FunctionRef FunctionReference `json:"functionRef"`
 
-	// Config is an optional, arbitrary Kubernetes resource (i.e. a resource
+	// Input is an optional, arbitrary Kubernetes resource (i.e. a resource
 	// with an apiVersion and kind) that will be passed to the Composition
-	// Function as the 'config' block of its FunctionIO.
+	// Function as the 'input' of its RunFunctionRequest.
 	// +optional
 	// +kubebuilder:pruning:PreserveUnknownFields
 	// +kubebuilder:validation:EmbeddedResource
-	Config *runtime.RawExtension `json:"config,omitempty"`
-
-	// Container configuration of this function.
-	// +optional
-	Container *ContainerFunction `json:"container,omitempty"`
-}
-
-// Validate this Function.
-func (f *Function) Validate() *field.Error {
-	if f.Type == FunctionTypeContainer {
-		if f.Container == nil {
-			return field.Required(field.NewPath("container"), "cannot be empty for type Container")
-		}
-		return nil
-	}
-	return field.Required(field.NewPath("type"), "the only supported type is Container")
+	Input *runtime.RawExtension `json:"input,omitempty"`
 }
 
-// A FunctionType is a type of Composition Function.
-type FunctionType string
-
-// FunctionType types.
-const (
-	// FunctionTypeContainer represents a Composition Function that is packaged
-	// as an OCI image and run in a container.
-	FunctionTypeContainer FunctionType = "Container"
-)
-
-// NOTE(negz): This is intentionally much more limited than corev1.Container.
-// This is because:
-//
-// * We always expect functions to be short-lived processes.
-// * We never expect functions to listen for incoming requests.
-// * We don't allow functions to mount volumes.
-
-// A ContainerFunction represents an Composition Function that is packaged as an
-// OCI image and run in a container.
-type ContainerFunction struct {
-	// Image specifies the OCI image in which the function is packaged. The
-	// image should include an entrypoint that reads a FunctionIO from stdin and
-	// emits it, optionally mutated, to stdout.
-	Image string `json:"image"`
-
-	// ImagePullPolicy defines the pull policy for the function image.
-	// +optional
-	// +kubebuilder:default=IfNotPresent
-	// +kubebuilder:validation:Enum="IfNotPresent";"Always";"Never"
-	ImagePullPolicy *corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
-
-	// ImagePullSecrets are used to pull images from private OCI registries.
-	// +optional
-	ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
-
-	// Timeout after which the Composition Function will be killed.
-	// +optional
-	// +kubebuilder:default="20s"
-	Timeout *metav1.Duration `json:"timeout,omitempty"`
-
-	// Network configuration for the Composition Function.
-	// +optional
-	Network *ContainerFunctionNetwork `json:"network,omitempty"`
-
-	// Resources that may be used by the Composition Function.
-	// +optional
-	Resources *ContainerFunctionResources `json:"resources,omitempty"`
-
-	// Runner configuration for the Composition Function.
-	// +optional
-	Runner *ContainerFunctionRunner `json:"runner,omitempty"`
-}
-
-// A ContainerFunctionNetworkPolicy specifies the network policy under which
-// a containerized Composition Function will run.
-type ContainerFunctionNetworkPolicy string
-
-const (
-	// ContainerFunctionNetworkPolicyIsolated specifies that the Composition
-	// Function will not have network access; i.e. invoked inside an isolated
-	// network namespace.
-	ContainerFunctionNetworkPolicyIsolated ContainerFunctionNetworkPolicy = "Isolated"
-
-	// ContainerFunctionNetworkPolicyRunner specifies that the Composition
-	// Function will have the same network access as its runner, i.e. share its
-	// runner's network namespace.
-	ContainerFunctionNetworkPolicyRunner ContainerFunctionNetworkPolicy = "Runner"
-)
-
-// ContainerFunctionNetwork represents configuration for a Composition Function.
-type ContainerFunctionNetwork struct {
-	// Policy specifies the network policy under which the Composition Function
-	// will run. Defaults to 'Isolated' - i.e. no network access. Specify
-	// 'Runner' to allow the function the same network access as
-	// its runner.
-	// +optional
-	// +kubebuilder:validation:Enum="Isolated";"Runner"
-	// +kubebuilder:default=Isolated
-	Policy *ContainerFunctionNetworkPolicy `json:"policy,omitempty"`
-}
-
-// ContainerFunctionResources represents compute resources that may be used by a
-// Composition Function.
-type ContainerFunctionResources struct {
-	// Limits specify the maximum compute resources that may be used by the
-	// Composition Function.
-	// +optional
-	Limits *ContainerFunctionResourceLimits `json:"limits,omitempty"`
-
-	// NOTE(negz): We don't presently have any runners that support scheduling,
-	// so we omit Requests for the time being.
-}
-
-// ContainerFunctionResourceLimits specify the maximum compute resources
-// that may be used by a Composition Function.
-type ContainerFunctionResourceLimits struct {
-	// CPU, in cores. (500m = .5 cores)
-	// +kubebuilder:default="100m"
-	// +optional
-	CPU *resource.Quantity `json:"cpu,omitempty"`
-
-	// Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024)
-	// +kubebuilder:default="128Mi"
-	// +optional
-	Memory *resource.Quantity `json:"memory,omitempty"`
-}
-
-// ContainerFunctionRunner represents runner configuration for a Composition
-// Function.
-type ContainerFunctionRunner struct {
-	// Endpoint specifies how and where Crossplane should reach the runner it
-	// uses to invoke containerized Composition Functions.
-	// +optional
-	// +kubebuilder:default="unix-abstract:crossplane/fn/default.sock"
-	Endpoint *string `json:"endpoint,omitempty"`
+// A FunctionReference references a Composition Function that may be used in a
+// Composition pipeline.
+type FunctionReference struct {
+	// Name of the referenced Function.
+	Name string `json:"name"`
 }
 
 // A StoreConfigReference references a secret store config that may be used to
diff --git a/apis/apiextensions/v1beta1/zz_generated.composition_revision_types.go b/apis/apiextensions/v1beta1/zz_generated.composition_revision_types.go
index ec6300306..3415ce384 100644
--- a/apis/apiextensions/v1beta1/zz_generated.composition_revision_types.go
+++ b/apis/apiextensions/v1beta1/zz_generated.composition_revision_types.go
@@ -43,33 +43,64 @@ type CompositionRevisionSpec struct {
 	// +immutable
 	CompositeTypeRef TypeReference `json:"compositeTypeRef"`
 
-	// PatchSets define a named set of patches that may be included by
-	// any resource in this Composition.
-	// PatchSets cannot themselves refer to other PatchSets.
+	// Mode controls what type or "mode" of Composition will be used.
+	//
+	// "Resources" (the default) indicates that a Composition uses what is
+	// commonly referred to as "Patch & Transform" or P&T composition. This mode
+	// of Composition uses an array of resources, each a template for a composed
+	// resource.
+	//
+	// "Pipeline" indicates that a Composition specifies a pipeline
+	// of Composition Functions, each of which is responsible for producing
+	// composed resources that Crossplane should create or update. THE PIPELINE
+	// MODE IS A BETA FEATURE. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
+	// +optional
+	// +kubebuilder:validation:Enum=Resources;Pipeline
+	// +kubebuilder:default=Resources
+	Mode *CompositionMode `json:"mode,omitempty"`
+
+	// PatchSets define a named set of patches that may be included by any
+	// resource in this Composition. PatchSets cannot themselves refer to other
+	// PatchSets.
+	//
+	// PatchSets are only used by the "Resources" mode of Composition. They
+	// are ignored by other modes.
 	// +optional
 	PatchSets []PatchSet `json:"patchSets,omitempty"`
 
 	// Environment configures the environment in which resources are rendered.
+	//
+	// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored
+	// unless the relevant Crossplane feature flag is enabled, and may be
+	// changed or removed without notice.
 	// +optional
 	Environment *EnvironmentConfiguration `json:"environment,omitempty"`
 
-	// Resources is the list of resource templates that will be used when a
+	// Resources is a list of resource templates that will be used when a
 	// composite resource referring to this composition is created.
+	//
+	// Resources are only used by the "Resources" mode of Composition. They are
+	// ignored by other modes.
 	// +optional
-	Resources []ComposedTemplate `json:"resources"`
-
-	// Functions is list of Composition Functions that will be used when a
-	// composite resource referring to this composition is created. At least one
-	// of resources and functions must be specified. If both are specified the
-	// resources will be rendered first, then passed to the functions for
-	// further processing.
+	Resources []ComposedTemplate `json:"resources,omitempty"`
+
+	// Pipeline is a list of composition function steps that will be used when a
+	// composite resource referring to this composition is created. One of
+	// resources and pipeline must be specified - you cannot specify both.
+	//
+	// The Pipeline is only used by the "Pipeline" mode of Composition. It is
+	// ignored by other modes.
+	//
+	// THIS IS A BETA FIELD. It is not honored if the relevant Crossplane
+	// feature flag is disabled.
 	// +optional
-	Functions []Function `json:"functions,omitempty"`
+	Pipeline []PipelineStep `json:"pipeline,omitempty"`
 
 	// WriteConnectionSecretsToNamespace specifies the namespace in which the
 	// connection secrets of composite resource dynamically provisioned using
 	// this composition will be created.
-	// This field is planned to be removed in a future release in favor of
+	// This field is planned to be replaced in a future release in favor of
 	// PublishConnectionDetailsWithStoreConfigRef. Currently, both could be
 	// set independently and connection details would be published to both
 	// without affecting each other as long as related fields at MR level
@@ -80,6 +111,10 @@ type CompositionRevisionSpec struct {
 	// PublishConnectionDetailsWithStoreConfig specifies the secret store config
 	// with which the connection details of composite resources dynamically
 	// provisioned using this composition will be published.
+	//
+	// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored
+	// unless the relevant Crossplane feature flag is enabled, and may be
+	// changed or removed without notice.
 	// +optional
 	// +kubebuilder:default={"name": "default"}
 	PublishConnectionDetailsWithStoreConfigRef *StoreConfigReference `json:"publishConnectionDetailsWithStoreConfigRef,omitempty"`
diff --git a/apis/apiextensions/v1beta1/zz_generated.deepcopy.go b/apis/apiextensions/v1beta1/zz_generated.deepcopy.go
index 14fc22df7..fe24f8813 100644
--- a/apis/apiextensions/v1beta1/zz_generated.deepcopy.go
+++ b/apis/apiextensions/v1beta1/zz_generated.deepcopy.go
@@ -22,9 +22,7 @@ package v1beta1
 
 import (
 	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
-	"k8s.io/api/core/v1"
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -173,6 +171,11 @@ func (in *CompositionRevisionList) DeepCopyObject() runtime.Object {
 func (in *CompositionRevisionSpec) DeepCopyInto(out *CompositionRevisionSpec) {
 	*out = *in
 	out.CompositeTypeRef = in.CompositeTypeRef
+	if in.Mode != nil {
+		in, out := &in.Mode, &out.Mode
+		*out = new(CompositionMode)
+		**out = **in
+	}
 	if in.PatchSets != nil {
 		in, out := &in.PatchSets, &out.PatchSets
 		*out = make([]PatchSet, len(*in))
@@ -192,9 +195,9 @@ func (in *CompositionRevisionSpec) DeepCopyInto(out *CompositionRevisionSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
-	if in.Functions != nil {
-		in, out := &in.Functions, &out.Functions
-		*out = make([]Function, len(*in))
+	if in.Pipeline != nil {
+		in, out := &in.Pipeline, &out.Pipeline
+		*out = make([]PipelineStep, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -277,136 +280,6 @@ func (in *ConnectionDetail) DeepCopy() *ConnectionDetail {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunction) DeepCopyInto(out *ContainerFunction) {
-	*out = *in
-	if in.ImagePullPolicy != nil {
-		in, out := &in.ImagePullPolicy, &out.ImagePullPolicy
-		*out = new(v1.PullPolicy)
-		**out = **in
-	}
-	if in.ImagePullSecrets != nil {
-		in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
-		*out = make([]v1.LocalObjectReference, len(*in))
-		copy(*out, *in)
-	}
-	if in.Timeout != nil {
-		in, out := &in.Timeout, &out.Timeout
-		*out = new(metav1.Duration)
-		**out = **in
-	}
-	if in.Network != nil {
-		in, out := &in.Network, &out.Network
-		*out = new(ContainerFunctionNetwork)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.Resources != nil {
-		in, out := &in.Resources, &out.Resources
-		*out = new(ContainerFunctionResources)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.Runner != nil {
-		in, out := &in.Runner, &out.Runner
-		*out = new(ContainerFunctionRunner)
-		(*in).DeepCopyInto(*out)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunction.
-func (in *ContainerFunction) DeepCopy() *ContainerFunction {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunction)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionNetwork) DeepCopyInto(out *ContainerFunctionNetwork) {
-	*out = *in
-	if in.Policy != nil {
-		in, out := &in.Policy, &out.Policy
-		*out = new(ContainerFunctionNetworkPolicy)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionNetwork.
-func (in *ContainerFunctionNetwork) DeepCopy() *ContainerFunctionNetwork {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionNetwork)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionResourceLimits) DeepCopyInto(out *ContainerFunctionResourceLimits) {
-	*out = *in
-	if in.CPU != nil {
-		in, out := &in.CPU, &out.CPU
-		x := (*in).DeepCopy()
-		*out = &x
-	}
-	if in.Memory != nil {
-		in, out := &in.Memory, &out.Memory
-		x := (*in).DeepCopy()
-		*out = &x
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionResourceLimits.
-func (in *ContainerFunctionResourceLimits) DeepCopy() *ContainerFunctionResourceLimits {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionResourceLimits)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionResources) DeepCopyInto(out *ContainerFunctionResources) {
-	*out = *in
-	if in.Limits != nil {
-		in, out := &in.Limits, &out.Limits
-		*out = new(ContainerFunctionResourceLimits)
-		(*in).DeepCopyInto(*out)
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionResources.
-func (in *ContainerFunctionResources) DeepCopy() *ContainerFunctionResources {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionResources)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ContainerFunctionRunner) DeepCopyInto(out *ContainerFunctionRunner) {
-	*out = *in
-	if in.Endpoint != nil {
-		in, out := &in.Endpoint, &out.Endpoint
-		*out = new(string)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerFunctionRunner.
-func (in *ContainerFunctionRunner) DeepCopy() *ContainerFunctionRunner {
-	if in == nil {
-		return nil
-	}
-	out := new(ContainerFunctionRunner)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ConvertTransform) DeepCopyInto(out *ConvertTransform) {
 	*out = *in
@@ -432,7 +305,7 @@ func (in *EnvironmentConfiguration) DeepCopyInto(out *EnvironmentConfiguration)
 	*out = *in
 	if in.DefaultData != nil {
 		in, out := &in.DefaultData, &out.DefaultData
-		*out = make(map[string]apiextensionsv1.JSON, len(*in))
+		*out = make(map[string]v1.JSON, len(*in))
 		for key, val := range *in {
 			(*out)[key] = *val.DeepCopy()
 		}
@@ -608,26 +481,16 @@ func (in *EnvironmentSourceSelectorLabelMatcher) DeepCopy() *EnvironmentSourceSe
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Function) DeepCopyInto(out *Function) {
+func (in *FunctionReference) DeepCopyInto(out *FunctionReference) {
 	*out = *in
-	if in.Config != nil {
-		in, out := &in.Config, &out.Config
-		*out = new(runtime.RawExtension)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.Container != nil {
-		in, out := &in.Container, &out.Container
-		*out = new(ContainerFunction)
-		(*in).DeepCopyInto(*out)
-	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Function.
-func (in *Function) DeepCopy() *Function {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionReference.
+func (in *FunctionReference) DeepCopy() *FunctionReference {
 	if in == nil {
 		return nil
 	}
-	out := new(Function)
+	out := new(FunctionReference)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -637,7 +500,7 @@ func (in *MapTransform) DeepCopyInto(out *MapTransform) {
 	*out = *in
 	if in.Pairs != nil {
 		in, out := &in.Pairs, &out.Pairs
-		*out = make(map[string]apiextensionsv1.JSON, len(*in))
+		*out = make(map[string]v1.JSON, len(*in))
 		for key, val := range *in {
 			(*out)[key] = *val.DeepCopy()
 		}
@@ -842,6 +705,27 @@ func (in *PatchSet) DeepCopy() *PatchSet {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PipelineStep) DeepCopyInto(out *PipelineStep) {
+	*out = *in
+	out.FunctionRef = in.FunctionRef
+	if in.Input != nil {
+		in, out := &in.Input, &out.Input
+		*out = new(runtime.RawExtension)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelineStep.
+func (in *PipelineStep) DeepCopy() *PipelineStep {
+	if in == nil {
+		return nil
+	}
+	out := new(PipelineStep)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ReadinessCheck) DeepCopyInto(out *ReadinessCheck) {
 	*out = *in
diff --git a/apis/generate.go b/apis/generate.go
index eaf424035..0a5f7afca 100644
--- a/apis/generate.go
+++ b/apis/generate.go
@@ -35,6 +35,7 @@ limitations under the License.
 //go:generate ../hack/duplicate_api_type.sh pkg/meta/v1/configuration_types.go pkg/meta/v1alpha1
 //go:generate ../hack/duplicate_api_type.sh pkg/meta/v1/provider_types.go pkg/meta/v1alpha1
 //go:generate ../hack/duplicate_api_type.sh pkg/meta/v1/meta.go pkg/meta/v1alpha1
+//go:generate ../hack/duplicate_api_type.sh pkg/meta/v1/meta.go pkg/meta/v1beta1
 
 // NOTE(negz): We generate deepcopy methods and CRDs for each API group
 // separately because there seems to be an undiagnosed bug in controller-runtime
@@ -46,12 +47,9 @@ limitations under the License.
 //go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./apiextensions/v1alpha1;./apiextensions/v1beta1;./apiextensions/v1 crd:crdVersions=v1 output:artifacts:config=../cluster/crds
 //go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./secrets/... crd:crdVersions=v1 output:artifacts:config=../cluster/crds
 
-// We generate the meta.pkg.crossplane.io and fn.apiextensions.crossplane.io
-// types separately as the generated CRDs are never installed, only used for API
-// documentation.
-// TODO(negz): fn.apiextensions.crossplane.io CRD generation doesn't appear to
-// work. Possibly because they wouldn't be valid - they lack object metadata.
-//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./pkg/meta/...;./apiextensions/fn/io/... crd:crdVersions=v1 output:artifacts:config=../cluster/meta
+// We generate the meta.pkg.crossplane.io types separately as the generated CRDs
+// are never installed, only used for API documentation.
+//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../hack/boilerplate.go.txt paths=./pkg/meta/... crd:crdVersions=v1 output:artifacts:config=../cluster/meta
 
 // Generate webhook manifests
 //go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen webhook paths=./pkg/v1alpha1;./pkg/v1beta1;./pkg/v1;./apiextensions/v1alpha1;./apiextensions/v1beta1;./apiextensions/v1 output:artifacts:config=../cluster/webhookconfigurations
diff --git a/apis/pkg/meta/v1alpha1/conversion.go b/apis/pkg/meta/v1alpha1/conversion.go
index 8419017ca..995f67021 100644
--- a/apis/pkg/meta/v1alpha1/conversion.go
+++ b/apis/pkg/meta/v1alpha1/conversion.go
@@ -30,9 +30,6 @@ const (
 
 	errWrongConvertToProvider   = "must convert to *v1.Provider"
 	errWrongConvertFromProvider = "must convert from *v1.Provider"
-
-	errWrongConvertToFunction   = "must convert to *v1alpha1.Function"
-	errWrongConvertFromFunction = "must convert from *v1alpha1.Function"
 )
 
 // A ToHubConverter converts v1alpha1 types to the 'hub' v1 type.
@@ -44,7 +41,6 @@ const (
 type ToHubConverter interface {
 	Configuration(in *Configuration) *v1.Configuration
 	Provider(in *Provider) *v1.Provider
-	Function(in *Function) *Function
 }
 
 // A FromHubConverter converts v1alpha1 types from the 'hub' v1 type.
@@ -56,7 +52,6 @@ type ToHubConverter interface {
 type FromHubConverter interface {
 	Configuration(in *v1.Configuration) *Configuration
 	Provider(in *v1.Provider) *Provider
-	Function(in *Function) *Function
 }
 
 // ConvertObjectMeta 'converts' ObjectMeta by producing a deepcopy. This
@@ -118,32 +113,3 @@ func (p *Provider) ConvertFrom(hub conversion.Hub) error {
 
 	return nil
 }
-
-// Hub marks this type as the conversion hub.
-func (f *Function) Hub() {}
-
-// ConvertTo converts this Configuration to the Hub version.
-func (f *Function) ConvertTo(hub conversion.Hub) error {
-	out, ok := hub.(*Function)
-	if !ok {
-		return errors.New(errWrongConvertToFunction)
-	}
-
-	conv := &GeneratedToHubConverter{}
-	*out = *conv.Function(f)
-
-	return nil
-}
-
-// ConvertFrom converts this Function from the Hub version.
-func (f *Function) ConvertFrom(hub conversion.Hub) error {
-	in, ok := hub.(*Function)
-	if !ok {
-		return errors.New(errWrongConvertFromFunction)
-	}
-
-	conv := &GeneratedFromHubConverter{}
-	*f = *conv.Function(in)
-
-	return nil
-}
diff --git a/apis/pkg/meta/v1alpha1/register.go b/apis/pkg/meta/v1alpha1/register.go
index 0447e11f2..41f37e2c0 100644
--- a/apis/pkg/meta/v1alpha1/register.go
+++ b/apis/pkg/meta/v1alpha1/register.go
@@ -56,16 +56,7 @@ var (
 	ConfigurationGroupVersionKind = SchemeGroupVersion.WithKind(ConfigurationKind)
 )
 
-// Function type metadata.
-var (
-	FunctionKind             = reflect.TypeOf(Function{}).Name()
-	FunctionGroupKind        = schema.GroupKind{Group: Group, Kind: FunctionKind}.String()
-	FunctionKindAPIVersion   = FunctionKind + "." + SchemeGroupVersion.String()
-	FunctionGroupVersionKind = SchemeGroupVersion.WithKind(FunctionKind)
-)
-
 func init() {
 	SchemeBuilder.Register(&Configuration{})
 	SchemeBuilder.Register(&Provider{})
-	SchemeBuilder.Register(&Function{})
 }
diff --git a/apis/pkg/meta/v1alpha1/zz_generated.conversion.go b/apis/pkg/meta/v1alpha1/zz_generated.conversion.go
index 6c4f52a8f..2c7fc1a52 100755
--- a/apis/pkg/meta/v1alpha1/zz_generated.conversion.go
+++ b/apis/pkg/meta/v1alpha1/zz_generated.conversion.go
@@ -21,17 +21,6 @@ func (c *GeneratedFromHubConverter) Configuration(source *v1.Configuration) *Con
 	}
 	return pV1alpha1Configuration
 }
-func (c *GeneratedFromHubConverter) Function(source *Function) *Function {
-	var pV1alpha1Function *Function
-	if source != nil {
-		var v1alpha1Function Function
-		v1alpha1Function.TypeMeta = c.v1TypeMetaToV1TypeMeta((*source).TypeMeta)
-		v1alpha1Function.ObjectMeta = ConvertObjectMeta((*source).ObjectMeta)
-		v1alpha1Function.Spec = c.v1alpha1FunctionSpecToV1alpha1FunctionSpec((*source).Spec)
-		pV1alpha1Function = &v1alpha1Function
-	}
-	return pV1alpha1Function
-}
 func (c *GeneratedFromHubConverter) Provider(source *v1.Provider) *Provider {
 	var pV1alpha1Provider *Provider
 	if source != nil {
@@ -52,15 +41,6 @@ func (c *GeneratedFromHubConverter) pV1CrossplaneConstraintsToPV1alpha1Crossplan
 	}
 	return pV1alpha1CrossplaneConstraints
 }
-func (c *GeneratedFromHubConverter) pV1alpha1CrossplaneConstraintsToPV1alpha1CrossplaneConstraints(source *CrossplaneConstraints) *CrossplaneConstraints {
-	var pV1alpha1CrossplaneConstraints *CrossplaneConstraints
-	if source != nil {
-		var v1alpha1CrossplaneConstraints CrossplaneConstraints
-		v1alpha1CrossplaneConstraints.Version = (*source).Version
-		pV1alpha1CrossplaneConstraints = &v1alpha1CrossplaneConstraints
-	}
-	return pV1alpha1CrossplaneConstraints
-}
 func (c *GeneratedFromHubConverter) v1ConfigurationSpecToV1alpha1ConfigurationSpec(source v1.ConfigurationSpec) ConfigurationSpec {
 	var v1alpha1ConfigurationSpec ConfigurationSpec
 	v1alpha1ConfigurationSpec.MetaSpec = c.v1MetaSpecToV1alpha1MetaSpec(source.MetaSpec)
@@ -176,53 +156,6 @@ func (c *GeneratedFromHubConverter) v1TypeMetaToV1TypeMeta(source v12.TypeMeta)
 	v1TypeMeta.APIVersion = source.APIVersion
 	return v1TypeMeta
 }
-func (c *GeneratedFromHubConverter) v1alpha1DependencyToV1alpha1Dependency(source Dependency) Dependency {
-	var v1alpha1Dependency Dependency
-	var pString *string
-	if source.Provider != nil {
-		xstring := *source.Provider
-		pString = &xstring
-	}
-	v1alpha1Dependency.Provider = pString
-	var pString2 *string
-	if source.Configuration != nil {
-		xstring2 := *source.Configuration
-		pString2 = &xstring2
-	}
-	v1alpha1Dependency.Configuration = pString2
-	var pString3 *string
-	if source.Function != nil {
-		xstring3 := *source.Function
-		pString3 = &xstring3
-	}
-	v1alpha1Dependency.Function = pString3
-	v1alpha1Dependency.Version = source.Version
-	return v1alpha1Dependency
-}
-func (c *GeneratedFromHubConverter) v1alpha1FunctionSpecToV1alpha1FunctionSpec(source FunctionSpec) FunctionSpec {
-	var v1alpha1FunctionSpec FunctionSpec
-	v1alpha1FunctionSpec.MetaSpec = c.v1alpha1MetaSpecToV1alpha1MetaSpec(source.MetaSpec)
-	var pString *string
-	if source.Image != nil {
-		xstring := *source.Image
-		pString = &xstring
-	}
-	v1alpha1FunctionSpec.Image = pString
-	return v1alpha1FunctionSpec
-}
-func (c *GeneratedFromHubConverter) v1alpha1MetaSpecToV1alpha1MetaSpec(source MetaSpec) MetaSpec {
-	var v1alpha1MetaSpec MetaSpec
-	v1alpha1MetaSpec.Crossplane = c.pV1alpha1CrossplaneConstraintsToPV1alpha1CrossplaneConstraints(source.Crossplane)
-	var v1alpha1DependencyList []Dependency
-	if source.DependsOn != nil {
-		v1alpha1DependencyList = make([]Dependency, len(source.DependsOn))
-		for i := 0; i < len(source.DependsOn); i++ {
-			v1alpha1DependencyList[i] = c.v1alpha1DependencyToV1alpha1Dependency(source.DependsOn[i])
-		}
-	}
-	v1alpha1MetaSpec.DependsOn = v1alpha1DependencyList
-	return v1alpha1MetaSpec
-}
 
 type GeneratedToHubConverter struct{}
 
@@ -237,17 +170,6 @@ func (c *GeneratedToHubConverter) Configuration(source *Configuration) *v1.Confi
 	}
 	return pV1Configuration
 }
-func (c *GeneratedToHubConverter) Function(source *Function) *Function {
-	var pV1alpha1Function *Function
-	if source != nil {
-		var v1alpha1Function Function
-		v1alpha1Function.TypeMeta = c.v1TypeMetaToV1TypeMeta((*source).TypeMeta)
-		v1alpha1Function.ObjectMeta = ConvertObjectMeta((*source).ObjectMeta)
-		v1alpha1Function.Spec = c.v1alpha1FunctionSpecToV1alpha1FunctionSpec((*source).Spec)
-		pV1alpha1Function = &v1alpha1Function
-	}
-	return pV1alpha1Function
-}
 func (c *GeneratedToHubConverter) Provider(source *Provider) *v1.Provider {
 	var pV1Provider *v1.Provider
 	if source != nil {
@@ -268,15 +190,6 @@ func (c *GeneratedToHubConverter) pV1alpha1CrossplaneConstraintsToPV1CrossplaneC
 	}
 	return pV1CrossplaneConstraints
 }
-func (c *GeneratedToHubConverter) pV1alpha1CrossplaneConstraintsToPV1alpha1CrossplaneConstraints(source *CrossplaneConstraints) *CrossplaneConstraints {
-	var pV1alpha1CrossplaneConstraints *CrossplaneConstraints
-	if source != nil {
-		var v1alpha1CrossplaneConstraints CrossplaneConstraints
-		v1alpha1CrossplaneConstraints.Version = (*source).Version
-		pV1alpha1CrossplaneConstraints = &v1alpha1CrossplaneConstraints
-	}
-	return pV1alpha1CrossplaneConstraints
-}
 func (c *GeneratedToHubConverter) v1PolicyRuleToV1PolicyRule(source v11.PolicyRule) v11.PolicyRule {
 	var v1PolicyRule v11.PolicyRule
 	var stringList []string
@@ -373,40 +286,6 @@ func (c *GeneratedToHubConverter) v1alpha1DependencyToV1Dependency(source Depend
 	v1Dependency.Version = source.Version
 	return v1Dependency
 }
-func (c *GeneratedToHubConverter) v1alpha1DependencyToV1alpha1Dependency(source Dependency) Dependency {
-	var v1alpha1Dependency Dependency
-	var pString *string
-	if source.Provider != nil {
-		xstring := *source.Provider
-		pString = &xstring
-	}
-	v1alpha1Dependency.Provider = pString
-	var pString2 *string
-	if source.Configuration != nil {
-		xstring2 := *source.Configuration
-		pString2 = &xstring2
-	}
-	v1alpha1Dependency.Configuration = pString2
-	var pString3 *string
-	if source.Function != nil {
-		xstring3 := *source.Function
-		pString3 = &xstring3
-	}
-	v1alpha1Dependency.Function = pString3
-	v1alpha1Dependency.Version = source.Version
-	return v1alpha1Dependency
-}
-func (c *GeneratedToHubConverter) v1alpha1FunctionSpecToV1alpha1FunctionSpec(source FunctionSpec) FunctionSpec {
-	var v1alpha1FunctionSpec FunctionSpec
-	v1alpha1FunctionSpec.MetaSpec = c.v1alpha1MetaSpecToV1alpha1MetaSpec(source.MetaSpec)
-	var pString *string
-	if source.Image != nil {
-		xstring := *source.Image
-		pString = &xstring
-	}
-	v1alpha1FunctionSpec.Image = pString
-	return v1alpha1FunctionSpec
-}
 func (c *GeneratedToHubConverter) v1alpha1MetaSpecToV1MetaSpec(source MetaSpec) v1.MetaSpec {
 	var v1MetaSpec v1.MetaSpec
 	v1MetaSpec.Crossplane = c.pV1alpha1CrossplaneConstraintsToPV1CrossplaneConstraints(source.Crossplane)
@@ -420,19 +299,6 @@ func (c *GeneratedToHubConverter) v1alpha1MetaSpecToV1MetaSpec(source MetaSpec)
 	v1MetaSpec.DependsOn = v1DependencyList
 	return v1MetaSpec
 }
-func (c *GeneratedToHubConverter) v1alpha1MetaSpecToV1alpha1MetaSpec(source MetaSpec) MetaSpec {
-	var v1alpha1MetaSpec MetaSpec
-	v1alpha1MetaSpec.Crossplane = c.pV1alpha1CrossplaneConstraintsToPV1alpha1CrossplaneConstraints(source.Crossplane)
-	var v1alpha1DependencyList []Dependency
-	if source.DependsOn != nil {
-		v1alpha1DependencyList = make([]Dependency, len(source.DependsOn))
-		for i := 0; i < len(source.DependsOn); i++ {
-			v1alpha1DependencyList[i] = c.v1alpha1DependencyToV1alpha1Dependency(source.DependsOn[i])
-		}
-	}
-	v1alpha1MetaSpec.DependsOn = v1alpha1DependencyList
-	return v1alpha1MetaSpec
-}
 func (c *GeneratedToHubConverter) v1alpha1ProviderSpecToV1ProviderSpec(source ProviderSpec) v1.ProviderSpec {
 	var v1ProviderSpec v1.ProviderSpec
 	v1ProviderSpec.Controller = c.v1alpha1ControllerSpecToV1ControllerSpec(source.Controller)
diff --git a/apis/pkg/meta/v1alpha1/zz_generated.deepcopy.go b/apis/pkg/meta/v1alpha1/zz_generated.deepcopy.go
index f060b2404..cba93e26c 100644
--- a/apis/pkg/meta/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/pkg/meta/v1alpha1/zz_generated.deepcopy.go
@@ -139,53 +139,6 @@ func (in *Dependency) DeepCopy() *Dependency {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Function) DeepCopyInto(out *Function) {
-	*out = *in
-	out.TypeMeta = in.TypeMeta
-	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	in.Spec.DeepCopyInto(&out.Spec)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Function.
-func (in *Function) DeepCopy() *Function {
-	if in == nil {
-		return nil
-	}
-	out := new(Function)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *Function) DeepCopyObject() runtime.Object {
-	if c := in.DeepCopy(); c != nil {
-		return c
-	}
-	return nil
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionSpec) DeepCopyInto(out *FunctionSpec) {
-	*out = *in
-	in.MetaSpec.DeepCopyInto(&out.MetaSpec)
-	if in.Image != nil {
-		in, out := &in.Image, &out.Image
-		*out = new(string)
-		**out = **in
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionSpec.
-func (in *FunctionSpec) DeepCopy() *FunctionSpec {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionSpec)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GeneratedFromHubConverter) DeepCopyInto(out *GeneratedFromHubConverter) {
 	*out = *in
diff --git a/apis/pkg/meta/v1beta1/conversion.go b/apis/pkg/meta/v1beta1/conversion.go
new file mode 100644
index 000000000..24fe9b14a
--- /dev/null
+++ b/apis/pkg/meta/v1beta1/conversion.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+// Hub marks this type as the conversion hub.
+func (p *Function) Hub() {}
diff --git a/apis/apiextensions/fn/io/v1alpha1/doc.go b/apis/pkg/meta/v1beta1/doc.go
similarity index 73%
rename from apis/apiextensions/fn/io/v1alpha1/doc.go
rename to apis/pkg/meta/v1beta1/doc.go
index 9eecc29bc..1674dabbe 100644
--- a/apis/apiextensions/fn/io/v1alpha1/doc.go
+++ b/apis/pkg/meta/v1beta1/doc.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2022 The Crossplane Authors.
+Copyright 2020 The Crossplane Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-// Package v1alpha1 contains meta types used to invoke Composition Functions.
+// Package v1beta1 contains meta types used to configure Crossplane packages.
 // +kubebuilder:object:generate=true
-// +groupName=fn.apiextensions.crossplane.io
-// +versionName=v1alpha1
-package v1alpha1
+// +groupName=meta.pkg.crossplane.io
+// +versionName=v1beta1
+package v1beta1
diff --git a/apis/pkg/meta/v1alpha1/interfaces.go b/apis/pkg/meta/v1beta1/function_interfaces.go
similarity index 55%
rename from apis/pkg/meta/v1alpha1/interfaces.go
rename to apis/pkg/meta/v1beta1/function_interfaces.go
index b092fbdaa..aec145335 100644
--- a/apis/pkg/meta/v1alpha1/interfaces.go
+++ b/apis/pkg/meta/v1beta1/function_interfaces.go
@@ -1,11 +1,25 @@
-package v1alpha1
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
 
 import (
-	"github.com/crossplane/crossplane/apis/pkg/meta/v1"
+	v1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
 )
 
-var _ v1.Pkg = &Function{}
-
 // GetCrossplaneConstraints gets the Function package's Crossplane version constraints.
 func (f *Function) GetCrossplaneConstraints() *v1.CrossplaneConstraints {
 	if f.Spec.MetaSpec.Crossplane == nil {
diff --git a/apis/pkg/meta/v1alpha1/function_types.go b/apis/pkg/meta/v1beta1/function_types.go
similarity index 98%
rename from apis/pkg/meta/v1alpha1/function_types.go
rename to apis/pkg/meta/v1beta1/function_types.go
index 1058e21f4..cd603b534 100644
--- a/apis/pkg/meta/v1alpha1/function_types.go
+++ b/apis/pkg/meta/v1beta1/function_types.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1alpha1
+package v1beta1
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/apis/apiextensions/fn/io/v1alpha1/register.go b/apis/pkg/meta/v1beta1/register.go
similarity index 65%
rename from apis/apiextensions/fn/io/v1alpha1/register.go
rename to apis/pkg/meta/v1beta1/register.go
index 90450c4ea..d5675481d 100644
--- a/apis/apiextensions/fn/io/v1alpha1/register.go
+++ b/apis/pkg/meta/v1beta1/register.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2022 The Crossplane Authors.
+Copyright 2020 The Crossplane Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1alpha1
+package v1beta1
 
 import (
 	"reflect"
@@ -23,12 +23,10 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/scheme"
 )
 
-// TODO(negz): Will we ever actually want to register this with a scheme?
-
-// Function type metadata.
+// Package type metadata.
 const (
-	Group   = "fn.apiextensions.crossplane.io"
-	Version = "v1alpha1"
+	Group   = "meta.pkg.crossplane.io"
+	Version = "v1beta1"
 )
 
 var (
@@ -42,14 +40,14 @@ var (
 	AddToScheme = SchemeBuilder.AddToScheme
 )
 
-// ResourceList type metadata.
+// Function type metadata.
 var (
-	FunctionIOKind             = reflect.TypeOf(FunctionIO{}).Name()
-	FunctionIOGroupKind        = schema.GroupKind{Group: Group, Kind: FunctionIOKind}.String()
-	FunctionIOKindAPIVersion   = FunctionIOKind + "." + SchemeGroupVersion.String()
-	FunctionIOGroupVersionKind = SchemeGroupVersion.WithKind(FunctionIOKind)
+	FunctionKind             = reflect.TypeOf(Function{}).Name()
+	FunctionGroupKind        = schema.GroupKind{Group: Group, Kind: FunctionKind}.String()
+	FunctionKindAPIVersion   = FunctionKind + "." + SchemeGroupVersion.String()
+	FunctionGroupVersionKind = SchemeGroupVersion.WithKind(FunctionKind)
 )
 
 func init() {
-	SchemeBuilder.Register(&FunctionIO{})
+	SchemeBuilder.Register(&Function{})
 }
diff --git a/apis/pkg/meta/v1beta1/zz_generated.deepcopy.go b/apis/pkg/meta/v1beta1/zz_generated.deepcopy.go
new file mode 100644
index 000000000..3411301bd
--- /dev/null
+++ b/apis/pkg/meta/v1beta1/zz_generated.deepcopy.go
@@ -0,0 +1,144 @@
+//go:build !ignore_autogenerated
+
+/*
+Copyright 2021 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CrossplaneConstraints) DeepCopyInto(out *CrossplaneConstraints) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossplaneConstraints.
+func (in *CrossplaneConstraints) DeepCopy() *CrossplaneConstraints {
+	if in == nil {
+		return nil
+	}
+	out := new(CrossplaneConstraints)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Dependency) DeepCopyInto(out *Dependency) {
+	*out = *in
+	if in.Provider != nil {
+		in, out := &in.Provider, &out.Provider
+		*out = new(string)
+		**out = **in
+	}
+	if in.Configuration != nil {
+		in, out := &in.Configuration, &out.Configuration
+		*out = new(string)
+		**out = **in
+	}
+	if in.Function != nil {
+		in, out := &in.Function, &out.Function
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dependency.
+func (in *Dependency) DeepCopy() *Dependency {
+	if in == nil {
+		return nil
+	}
+	out := new(Dependency)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Function) DeepCopyInto(out *Function) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Function.
+func (in *Function) DeepCopy() *Function {
+	if in == nil {
+		return nil
+	}
+	out := new(Function)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Function) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FunctionSpec) DeepCopyInto(out *FunctionSpec) {
+	*out = *in
+	in.MetaSpec.DeepCopyInto(&out.MetaSpec)
+	if in.Image != nil {
+		in, out := &in.Image, &out.Image
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionSpec.
+func (in *FunctionSpec) DeepCopy() *FunctionSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(FunctionSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MetaSpec) DeepCopyInto(out *MetaSpec) {
+	*out = *in
+	if in.Crossplane != nil {
+		in, out := &in.Crossplane, &out.Crossplane
+		*out = new(CrossplaneConstraints)
+		**out = **in
+	}
+	if in.DependsOn != nil {
+		in, out := &in.DependsOn, &out.DependsOn
+		*out = make([]Dependency, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetaSpec.
+func (in *MetaSpec) DeepCopy() *MetaSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(MetaSpec)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/apis/pkg/meta/v1beta1/zz_generated.meta.go b/apis/pkg/meta/v1beta1/zz_generated.meta.go
new file mode 100644
index 000000000..6fee25638
--- /dev/null
+++ b/apis/pkg/meta/v1beta1/zz_generated.meta.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2020 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Generated from pkg/meta/v1/meta.go by ../hack/duplicate_api_type.sh. DO NOT EDIT.
+
+package v1beta1
+
+// MetaSpec are fields that every meta package type must implement.
+type MetaSpec struct {
+	// Semantic version constraints of Crossplane that package is compatible with.
+	Crossplane *CrossplaneConstraints `json:"crossplane,omitempty"`
+
+	// Dependencies on other packages.
+	DependsOn []Dependency `json:"dependsOn,omitempty"`
+}
+
+// CrossplaneConstraints specifies a packages compatibility with Crossplane versions.
+type CrossplaneConstraints struct {
+	// Semantic version constraints of Crossplane that package is compatible with.
+	Version string `json:"version"`
+}
+
+// Dependency is a dependency on another package. One of Provider or Configuration may be supplied.
+type Dependency struct {
+	// Provider is the name of a Provider package image.
+	Provider *string `json:"provider,omitempty"`
+
+	// Configuration is the name of a Configuration package image.
+	Configuration *string `json:"configuration,omitempty"`
+
+	// Function is the name of a Function package image.
+	Function *string `json:"function,omitempty"`
+
+	// Version is the semantic version constraints of the dependency image.
+	Version string `json:"version"`
+}
diff --git a/apis/pkg/v1/interfaces.go b/apis/pkg/v1/interfaces.go
index 2f76096ad..4ad0d5235 100644
--- a/apis/pkg/v1/interfaces.go
+++ b/apis/pkg/v1/interfaces.go
@@ -43,10 +43,6 @@ const (
 	LabelProviderFamily = "pkg.crossplane.io/provider-family"
 )
 
-// RevisionActivationPolicy indicates how a package should activate its
-// revisions.
-type RevisionActivationPolicy string
-
 var (
 	// AutomaticActivation indicates that package should automatically activate
 	// package revisions.
@@ -65,9 +61,6 @@ func RefNames(refs []corev1.LocalObjectReference) []string {
 	return stringRefs
 }
 
-var _ Package = &Provider{}
-var _ Package = &Configuration{}
-
 // Package is the interface satisfied by package types.
 // +k8s:deepcopy-gen=false
 type Package interface {
@@ -370,9 +363,6 @@ func (p *Configuration) GetTLSClientSecretName() *string {
 	return nil
 }
 
-var _ PackageRevision = &ProviderRevision{}
-var _ PackageRevision = &ConfigurationRevision{}
-
 // PackageRevision is the interface satisfied by package revision types.
 // +k8s:deepcopy-gen=false
 type PackageRevision interface {
@@ -774,9 +764,6 @@ func (p *ConfigurationRevision) SetCommonLabels(l map[string]string) {
 	p.Spec.CommonLabels = l
 }
 
-var _ PackageRevisionList = &ProviderRevisionList{}
-var _ PackageRevisionList = &ConfigurationRevisionList{}
-
 // PackageRevisionList is the interface satisfied by package revision list
 // types.
 // +k8s:deepcopy-gen=false
diff --git a/apis/pkg/v1/package_types.go b/apis/pkg/v1/package_types.go
index 5018311c1..070080a74 100644
--- a/apis/pkg/v1/package_types.go
+++ b/apis/pkg/v1/package_types.go
@@ -18,6 +18,10 @@ package v1
 
 import corev1 "k8s.io/api/core/v1"
 
+// RevisionActivationPolicy indicates how a package should activate its
+// revisions.
+type RevisionActivationPolicy string
+
 // PackageSpec specifies the desired state of a Package.
 type PackageSpec struct {
 	// Package is the name of the package that is being requested.
diff --git a/apis/pkg/v1/provider_types.go b/apis/pkg/v1/provider_types.go
index 5a80c9d50..654516946 100644
--- a/apis/pkg/v1/provider_types.go
+++ b/apis/pkg/v1/provider_types.go
@@ -53,13 +53,6 @@ type ProviderSpec struct {
 	ControllerConfigReference *ControllerConfigReference `json:"controllerConfigRef,omitempty"`
 }
 
-// A ControllerConfigReference to a ControllerConfig resource that will be used
-// to configure the packaged controller Deployment.
-type ControllerConfigReference struct {
-	// Name of the ControllerConfig.
-	Name string `json:"name"`
-}
-
 // ProviderStatus represents the observed state of a Provider.
 type ProviderStatus struct {
 	xpv1.ConditionedStatus `json:",inline"`
diff --git a/apis/pkg/v1/revision_types.go b/apis/pkg/v1/revision_types.go
index 5995a22a1..9a445f84b 100644
--- a/apis/pkg/v1/revision_types.go
+++ b/apis/pkg/v1/revision_types.go
@@ -117,6 +117,9 @@ type PackageRevisionSpec struct {
 type PackageRevisionStatus struct {
 	xpv1.ConditionedStatus `json:",inline"`
 
+	// TODO(negz): A Function doesn't have a 'controller'. Ideally we'd use a
+	// more generic name than 'ControllerRef'.
+
 	// ControllerRef references the controller (e.g. Deployment), if any, that
 	// is responsible for reconciling the objects this package revision
 	// installed.
@@ -142,3 +145,10 @@ type ControllerReference struct {
 	// Name of the controller.
 	Name string `json:"name"`
 }
+
+// A ControllerConfigReference to a ControllerConfig resource that will be used
+// to configure the packaged controller Deployment.
+type ControllerConfigReference struct {
+	// Name of the ControllerConfig.
+	Name string `json:"name"`
+}
diff --git a/apis/pkg/v1alpha1/register.go b/apis/pkg/v1alpha1/register.go
index 166300a8a..63509f215 100644
--- a/apis/pkg/v1alpha1/register.go
+++ b/apis/pkg/v1alpha1/register.go
@@ -48,24 +48,6 @@ var (
 	ControllerConfigGroupVersionKind = SchemeGroupVersion.WithKind(ControllerConfigKind)
 )
 
-// Function type metadata.
-var (
-	FunctionKind             = reflect.TypeOf(Function{}).Name()
-	FunctionGroupKind        = schema.GroupKind{Group: Group, Kind: FunctionKind}.String()
-	FunctionKindAPIVersion   = FunctionKind + "." + SchemeGroupVersion.String()
-	FunctionGroupVersionKind = SchemeGroupVersion.WithKind(FunctionKind)
-)
-
-// FunctionRevision type metadata.
-var (
-	FunctionRevisionKind             = reflect.TypeOf(FunctionRevision{}).Name()
-	FunctionRevisionGroupKind        = schema.GroupKind{Group: Group, Kind: FunctionRevisionKind}.String()
-	FunctionRevisionKindAPIVersion   = FunctionRevisionKind + "." + SchemeGroupVersion.String()
-	FunctionRevisionGroupVersionKind = SchemeGroupVersion.WithKind(FunctionRevisionKind)
-)
-
 func init() {
 	SchemeBuilder.Register(&ControllerConfig{}, &ControllerConfigList{})
-	SchemeBuilder.Register(&Function{}, &FunctionList{})
-	SchemeBuilder.Register(&FunctionRevision{}, &FunctionRevisionList{})
 }
diff --git a/apis/pkg/v1alpha1/zz_generated.deepcopy.go b/apis/pkg/v1alpha1/zz_generated.deepcopy.go
index 6cadaaac2..4cf439f8f 100644
--- a/apis/pkg/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/pkg/v1alpha1/zz_generated.deepcopy.go
@@ -215,173 +215,6 @@ func (in *ControllerConfigSpec) DeepCopy() *ControllerConfigSpec {
 	return out
 }
 
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Function) DeepCopyInto(out *Function) {
-	*out = *in
-	out.TypeMeta = in.TypeMeta
-	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	in.Spec.DeepCopyInto(&out.Spec)
-	in.Status.DeepCopyInto(&out.Status)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Function.
-func (in *Function) DeepCopy() *Function {
-	if in == nil {
-		return nil
-	}
-	out := new(Function)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *Function) DeepCopyObject() runtime.Object {
-	if c := in.DeepCopy(); c != nil {
-		return c
-	}
-	return nil
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionList) DeepCopyInto(out *FunctionList) {
-	*out = *in
-	out.TypeMeta = in.TypeMeta
-	in.ListMeta.DeepCopyInto(&out.ListMeta)
-	if in.Items != nil {
-		in, out := &in.Items, &out.Items
-		*out = make([]Function, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionList.
-func (in *FunctionList) DeepCopy() *FunctionList {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionList)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *FunctionList) DeepCopyObject() runtime.Object {
-	if c := in.DeepCopy(); c != nil {
-		return c
-	}
-	return nil
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionRevision) DeepCopyInto(out *FunctionRevision) {
-	*out = *in
-	out.TypeMeta = in.TypeMeta
-	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	in.Spec.DeepCopyInto(&out.Spec)
-	in.Status.DeepCopyInto(&out.Status)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevision.
-func (in *FunctionRevision) DeepCopy() *FunctionRevision {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionRevision)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *FunctionRevision) DeepCopyObject() runtime.Object {
-	if c := in.DeepCopy(); c != nil {
-		return c
-	}
-	return nil
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionRevisionList) DeepCopyInto(out *FunctionRevisionList) {
-	*out = *in
-	out.TypeMeta = in.TypeMeta
-	in.ListMeta.DeepCopyInto(&out.ListMeta)
-	if in.Items != nil {
-		in, out := &in.Items, &out.Items
-		*out = make([]FunctionRevision, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
-	}
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevisionList.
-func (in *FunctionRevisionList) DeepCopy() *FunctionRevisionList {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionRevisionList)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *FunctionRevisionList) DeepCopyObject() runtime.Object {
-	if c := in.DeepCopy(); c != nil {
-		return c
-	}
-	return nil
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionRevisionStatus) DeepCopyInto(out *FunctionRevisionStatus) {
-	*out = *in
-	in.PackageRevisionStatus.DeepCopyInto(&out.PackageRevisionStatus)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevisionStatus.
-func (in *FunctionRevisionStatus) DeepCopy() *FunctionRevisionStatus {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionRevisionStatus)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionSpec) DeepCopyInto(out *FunctionSpec) {
-	*out = *in
-	in.PackageSpec.DeepCopyInto(&out.PackageSpec)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionSpec.
-func (in *FunctionSpec) DeepCopy() *FunctionSpec {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionSpec)
-	in.DeepCopyInto(out)
-	return out
-}
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *FunctionStatus) DeepCopyInto(out *FunctionStatus) {
-	*out = *in
-	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
-	out.PackageStatus = in.PackageStatus
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionStatus.
-func (in *FunctionStatus) DeepCopy() *FunctionStatus {
-	if in == nil {
-		return nil
-	}
-	out := new(FunctionStatus)
-	in.DeepCopyInto(out)
-	return out
-}
-
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PodObjectMeta) DeepCopyInto(out *PodObjectMeta) {
 	*out = *in
diff --git a/apis/pkg/v1alpha1/interfaces.go b/apis/pkg/v1beta1/function_interfaces.go
similarity index 93%
rename from apis/pkg/v1alpha1/interfaces.go
rename to apis/pkg/v1beta1/function_interfaces.go
index 9c6ef3abb..5d5912819 100644
--- a/apis/pkg/v1alpha1/interfaces.go
+++ b/apis/pkg/v1beta1/function_interfaces.go
@@ -1,11 +1,27 @@
-package v1alpha1
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
 
 import (
 	corev1 "k8s.io/api/core/v1"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 
-	"github.com/crossplane/crossplane/apis/pkg/v1"
+	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 )
 
 // GetCondition of this Function.
@@ -136,8 +152,6 @@ func (f *Function) GetTLSClientSecretName() *string {
 	return nil
 }
 
-var _ v1.PackageRevisionList = &FunctionRevisionList{}
-
 // GetCondition of this FunctionRevision.
 func (r *FunctionRevision) GetCondition(ct xpv1.ConditionType) xpv1.Condition {
 	return r.Status.GetCondition(ct)
diff --git a/apis/pkg/v1alpha1/function_types.go b/apis/pkg/v1beta1/function_types.go
similarity index 89%
rename from apis/pkg/v1alpha1/function_types.go
rename to apis/pkg/v1beta1/function_types.go
index 607eaec8a..69127f9f0 100644
--- a/apis/pkg/v1alpha1/function_types.go
+++ b/apis/pkg/v1beta1/function_types.go
@@ -14,13 +14,19 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package v1alpha1
+package v1beta1
 
 import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 
+	// TODO(negz): Ideally our v1beta1 package wouldn't import types from v1, as
+	// this strongly couples the types. This would make life difficult if we
+	// wanted to evolve this package in a different direction from the current
+	// v1 implementation. Unfortunately the package manager implementation
+	// requires any type that is reconciled as a package (or revision) to
+	// satisfy interfaces that involve returning v1 types.
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 )
 
@@ -53,10 +59,6 @@ type FunctionSpec struct {
 type FunctionStatus struct {
 	xpv1.ConditionedStatus `json:",inline"`
 	v1.PackageStatus       `json:",inline"`
-
-	// Endpoint is the gRPC endpoint where Crossplane will send
-	// RunFunctionRequests.
-	Endpoint string `json:"endpoint,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/apis/pkg/v1beta1/register.go b/apis/pkg/v1beta1/register.go
index dc966c160..9fe16af08 100644
--- a/apis/pkg/v1beta1/register.go
+++ b/apis/pkg/v1beta1/register.go
@@ -48,6 +48,24 @@ var (
 	LockGroupVersionKind = SchemeGroupVersion.WithKind(LockKind)
 )
 
+// Function type metadata.
+var (
+	FunctionKind             = reflect.TypeOf(Function{}).Name()
+	FunctionGroupKind        = schema.GroupKind{Group: Group, Kind: FunctionKind}.String()
+	FunctionKindAPIVersion   = FunctionKind + "." + SchemeGroupVersion.String()
+	FunctionGroupVersionKind = SchemeGroupVersion.WithKind(FunctionKind)
+)
+
+// FunctionRevision type metadata.
+var (
+	FunctionRevisionKind             = reflect.TypeOf(FunctionRevision{}).Name()
+	FunctionRevisionGroupKind        = schema.GroupKind{Group: Group, Kind: FunctionRevisionKind}.String()
+	FunctionRevisionKindAPIVersion   = FunctionRevisionKind + "." + SchemeGroupVersion.String()
+	FunctionRevisionGroupVersionKind = SchemeGroupVersion.WithKind(FunctionRevisionKind)
+)
+
 func init() {
 	SchemeBuilder.Register(&Lock{}, &LockList{})
+	SchemeBuilder.Register(&Function{}, &FunctionList{})
+	SchemeBuilder.Register(&FunctionRevision{}, &FunctionRevisionList{})
 }
diff --git a/apis/pkg/v1beta1/revision_types.go b/apis/pkg/v1beta1/revision_types.go
deleted file mode 100644
index dc223ca81..000000000
--- a/apis/pkg/v1beta1/revision_types.go
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
-Copyright 2020 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package v1beta1
-
-import (
-	corev1 "k8s.io/api/core/v1"
-	rbacv1 "k8s.io/api/rbac/v1"
-
-	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
-)
-
-// PackageRevisionDesiredState is the desired state of the package revision.
-type PackageRevisionDesiredState string
-
-const (
-	// PackageRevisionActive is an active package revision.
-	PackageRevisionActive PackageRevisionDesiredState = "Active"
-
-	// PackageRevisionInactive is an inactive package revision.
-	PackageRevisionInactive PackageRevisionDesiredState = "Inactive"
-)
-
-// PackageRevisionSpec specifies the desired state of a PackageRevision.
-type PackageRevisionSpec struct {
-	// ControllerConfigRef references a ControllerConfig resource that will be
-	// used to configure the packaged controller Deployment.
-	// +optional
-	ControllerConfigReference *xpv1.Reference `json:"controllerConfigRef,omitempty"`
-
-	// DesiredState of the PackageRevision. Can be either Active or Inactive.
-	DesiredState PackageRevisionDesiredState `json:"desiredState"`
-
-	// Package image used by install Pod to extract package contents.
-	Package string `json:"image"`
-
-	// PackagePullSecrets are named secrets in the same namespace that can be
-	// used to fetch packages from private registries. They are also applied to
-	// any images pulled for the package, such as a provider's controller image.
-	// +optional
-	PackagePullSecrets []corev1.LocalObjectReference `json:"packagePullSecrets,omitempty"`
-
-	// PackagePullPolicy defines the pull policy for the package. It is also
-	// applied to any images pulled for the package, such as a provider's
-	// controller image.
-	// Default is IfNotPresent.
-	// +optional
-	// +kubebuilder:default=IfNotPresent
-	PackagePullPolicy *corev1.PullPolicy `json:"packagePullPolicy,omitempty"`
-
-	// Revision number. Indicates when the revision will be garbage collected
-	// based on the parent's RevisionHistoryLimit.
-	Revision int64 `json:"revision"`
-
-	// IgnoreCrossplaneConstraints indicates to the package manager whether to
-	// honor Crossplane version constrains specified by the package.
-	// Default is false.
-	// +optional
-	// +kubebuilder:default=false
-	IgnoreCrossplaneConstraints *bool `json:"ignoreCrossplaneConstraints,omitempty"`
-
-	// SkipDependencyResolution indicates to the package manager whether to skip
-	// resolving dependencies for a package. Setting this value to true may have
-	// unintended consequences.
-	// Default is false.
-	// +optional
-	// +kubebuilder:default=false
-	SkipDependencyResolution *bool `json:"skipDependencyResolution,omitempty"`
-}
-
-// PackageRevisionStatus represents the observed state of a PackageRevision.
-type PackageRevisionStatus struct {
-	xpv1.ConditionedStatus `json:",inline"`
-	ControllerRef          xpv1.Reference `json:"controllerRef,omitempty"`
-
-	// References to objects owned by PackageRevision.
-	ObjectRefs []xpv1.TypedReference `json:"objectRefs,omitempty"`
-
-	// Dependency information.
-	FoundDependencies     int64 `json:"foundDependencies,omitempty"`
-	InstalledDependencies int64 `json:"installedDependencies,omitempty"`
-	InvalidDependencies   int64 `json:"invalidDependencies,omitempty"`
-
-	// PermissionRequests made by this package. The package declares that its
-	// controller needs these permissions to run. The RBAC manager is
-	// responsible for granting them.
-	PermissionRequests []rbacv1.PolicyRule `json:"permissionRequests,omitempty"`
-}
diff --git a/apis/pkg/v1beta1/zz_generated.deepcopy.go b/apis/pkg/v1beta1/zz_generated.deepcopy.go
index 70a26b737..586253d3f 100644
--- a/apis/pkg/v1beta1/zz_generated.deepcopy.go
+++ b/apis/pkg/v1beta1/zz_generated.deepcopy.go
@@ -21,9 +21,6 @@ limitations under the License.
 package v1beta1
 
 import (
-	"github.com/crossplane/crossplane-runtime/apis/common/v1"
-	corev1 "k8s.io/api/core/v1"
-	rbacv1 "k8s.io/api/rbac/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
@@ -43,31 +40,58 @@ func (in *Dependency) DeepCopy() *Dependency {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Lock) DeepCopyInto(out *Lock) {
+func (in *Function) DeepCopyInto(out *Function) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	if in.Packages != nil {
-		in, out := &in.Packages, &out.Packages
-		*out = make([]LockPackage, len(*in))
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Function.
+func (in *Function) DeepCopy() *Function {
+	if in == nil {
+		return nil
+	}
+	out := new(Function)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Function) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FunctionList) DeepCopyInto(out *FunctionList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Function, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lock.
-func (in *Lock) DeepCopy() *Lock {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionList.
+func (in *FunctionList) DeepCopy() *FunctionList {
 	if in == nil {
 		return nil
 	}
-	out := new(Lock)
+	out := new(FunctionList)
 	in.DeepCopyInto(out)
 	return out
 }
 
 // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *Lock) DeepCopyObject() runtime.Object {
+func (in *FunctionList) DeepCopyObject() runtime.Object {
 	if c := in.DeepCopy(); c != nil {
 		return c
 	}
@@ -75,31 +99,58 @@ func (in *Lock) DeepCopyObject() runtime.Object {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *LockList) DeepCopyInto(out *LockList) {
+func (in *FunctionRevision) DeepCopyInto(out *FunctionRevision) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevision.
+func (in *FunctionRevision) DeepCopy() *FunctionRevision {
+	if in == nil {
+		return nil
+	}
+	out := new(FunctionRevision)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *FunctionRevision) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FunctionRevisionList) DeepCopyInto(out *FunctionRevisionList) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ListMeta.DeepCopyInto(&out.ListMeta)
 	if in.Items != nil {
 		in, out := &in.Items, &out.Items
-		*out = make([]Lock, len(*in))
+		*out = make([]FunctionRevision, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockList.
-func (in *LockList) DeepCopy() *LockList {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevisionList.
+func (in *FunctionRevisionList) DeepCopy() *FunctionRevisionList {
 	if in == nil {
 		return nil
 	}
-	out := new(LockList)
+	out := new(FunctionRevisionList)
 	in.DeepCopyInto(out)
 	return out
 }
 
 // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
-func (in *LockList) DeepCopyObject() runtime.Object {
+func (in *FunctionRevisionList) DeepCopyObject() runtime.Object {
 	if c := in.DeepCopy(); c != nil {
 		return c
 	}
@@ -107,90 +158,134 @@ func (in *LockList) DeepCopyObject() runtime.Object {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *LockPackage) DeepCopyInto(out *LockPackage) {
+func (in *FunctionRevisionStatus) DeepCopyInto(out *FunctionRevisionStatus) {
 	*out = *in
-	if in.Dependencies != nil {
-		in, out := &in.Dependencies, &out.Dependencies
-		*out = make([]Dependency, len(*in))
-		copy(*out, *in)
-	}
+	in.PackageRevisionStatus.DeepCopyInto(&out.PackageRevisionStatus)
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockPackage.
-func (in *LockPackage) DeepCopy() *LockPackage {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionRevisionStatus.
+func (in *FunctionRevisionStatus) DeepCopy() *FunctionRevisionStatus {
 	if in == nil {
 		return nil
 	}
-	out := new(LockPackage)
+	out := new(FunctionRevisionStatus)
 	in.DeepCopyInto(out)
 	return out
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *PackageRevisionSpec) DeepCopyInto(out *PackageRevisionSpec) {
+func (in *FunctionSpec) DeepCopyInto(out *FunctionSpec) {
 	*out = *in
-	if in.ControllerConfigReference != nil {
-		in, out := &in.ControllerConfigReference, &out.ControllerConfigReference
-		*out = new(v1.Reference)
-		(*in).DeepCopyInto(*out)
-	}
-	if in.PackagePullSecrets != nil {
-		in, out := &in.PackagePullSecrets, &out.PackagePullSecrets
-		*out = make([]corev1.LocalObjectReference, len(*in))
-		copy(*out, *in)
-	}
-	if in.PackagePullPolicy != nil {
-		in, out := &in.PackagePullPolicy, &out.PackagePullPolicy
-		*out = new(corev1.PullPolicy)
-		**out = **in
+	in.PackageSpec.DeepCopyInto(&out.PackageSpec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionSpec.
+func (in *FunctionSpec) DeepCopy() *FunctionSpec {
+	if in == nil {
+		return nil
 	}
-	if in.IgnoreCrossplaneConstraints != nil {
-		in, out := &in.IgnoreCrossplaneConstraints, &out.IgnoreCrossplaneConstraints
-		*out = new(bool)
-		**out = **in
+	out := new(FunctionSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FunctionStatus) DeepCopyInto(out *FunctionStatus) {
+	*out = *in
+	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
+	out.PackageStatus = in.PackageStatus
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FunctionStatus.
+func (in *FunctionStatus) DeepCopy() *FunctionStatus {
+	if in == nil {
+		return nil
 	}
-	if in.SkipDependencyResolution != nil {
-		in, out := &in.SkipDependencyResolution, &out.SkipDependencyResolution
-		*out = new(bool)
-		**out = **in
+	out := new(FunctionStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Lock) DeepCopyInto(out *Lock) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	if in.Packages != nil {
+		in, out := &in.Packages, &out.Packages
+		*out = make([]LockPackage, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
 	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageRevisionSpec.
-func (in *PackageRevisionSpec) DeepCopy() *PackageRevisionSpec {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lock.
+func (in *Lock) DeepCopy() *Lock {
 	if in == nil {
 		return nil
 	}
-	out := new(PackageRevisionSpec)
+	out := new(Lock)
 	in.DeepCopyInto(out)
 	return out
 }
 
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Lock) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *PackageRevisionStatus) DeepCopyInto(out *PackageRevisionStatus) {
+func (in *LockList) DeepCopyInto(out *LockList) {
 	*out = *in
-	in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
-	in.ControllerRef.DeepCopyInto(&out.ControllerRef)
-	if in.ObjectRefs != nil {
-		in, out := &in.ObjectRefs, &out.ObjectRefs
-		*out = make([]v1.TypedReference, len(*in))
-		copy(*out, *in)
-	}
-	if in.PermissionRequests != nil {
-		in, out := &in.PermissionRequests, &out.PermissionRequests
-		*out = make([]rbacv1.PolicyRule, len(*in))
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Lock, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageRevisionStatus.
-func (in *PackageRevisionStatus) DeepCopy() *PackageRevisionStatus {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockList.
+func (in *LockList) DeepCopy() *LockList {
 	if in == nil {
 		return nil
 	}
-	out := new(PackageRevisionStatus)
+	out := new(LockList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *LockList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LockPackage) DeepCopyInto(out *LockPackage) {
+	*out = *in
+	if in.Dependencies != nil {
+		in, out := &in.Dependencies, &out.Dependencies
+		*out = make([]Dependency, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockPackage.
+func (in *LockPackage) DeepCopy() *LockPackage {
+	if in == nil {
+		return nil
+	}
+	out := new(LockPackage)
 	in.DeepCopyInto(out)
 	return out
 }
diff --git a/cluster/crds/apiextensions.crossplane.io_compositionrevisions.yaml b/cluster/crds/apiextensions.crossplane.io_compositionrevisions.yaml
index 313a65b55..a1f4ce411 100644
--- a/cluster/crds/apiextensions.crossplane.io_compositionrevisions.yaml
+++ b/cluster/crds/apiextensions.crossplane.io_compositionrevisions.yaml
@@ -67,8 +67,10 @@ spec:
                 - kind
                 type: object
               environment:
-                description: Environment configures the environment in which resources
-                  are rendered.
+                description: "Environment configures the environment in which resources
+                  are rendered. \n THIS IS AN ALPHA FIELD. Do not use it in production.
+                  It is not honored unless the relevant Crossplane feature flag is
+                  enabled, and may be changed or removed without notice."
                 properties:
                   defaultData:
                     additionalProperties:
@@ -522,135 +524,27 @@ spec:
                         type: string
                     type: object
                 type: object
-              functions:
-                description: Functions is list of Composition Functions that will
-                  be used when a composite resource referring to this composition
-                  is created. At least one of resources and functions must be specified.
-                  If both are specified the resources will be rendered first, then
-                  passed to the functions for further processing.
-                items:
-                  description: A Function represents a Composition Function.
-                  properties:
-                    config:
-                      description: Config is an optional, arbitrary Kubernetes resource
-                        (i.e. a resource with an apiVersion and kind) that will be
-                        passed to the Composition Function as the 'config' block of
-                        its FunctionIO.
-                      type: object
-                      x-kubernetes-embedded-resource: true
-                      x-kubernetes-preserve-unknown-fields: true
-                    container:
-                      description: Container configuration of this function.
-                      properties:
-                        image:
-                          description: Image specifies the OCI image in which the
-                            function is packaged. The image should include an entrypoint
-                            that reads a FunctionIO from stdin and emits it, optionally
-                            mutated, to stdout.
-                          type: string
-                        imagePullPolicy:
-                          default: IfNotPresent
-                          description: ImagePullPolicy defines the pull policy for
-                            the function image.
-                          enum:
-                          - IfNotPresent
-                          - Always
-                          - Never
-                          type: string
-                        imagePullSecrets:
-                          description: ImagePullSecrets are used to pull images from
-                            private OCI registries.
-                          items:
-                            description: LocalObjectReference contains enough information
-                              to let you locate the referenced object inside the same
-                              namespace.
-                            properties:
-                              name:
-                                description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-                                  TODO: Add other useful fields. apiVersion, kind,
-                                  uid?'
-                                type: string
-                            type: object
-                            x-kubernetes-map-type: atomic
-                          type: array
-                        network:
-                          description: Network configuration for the Composition Function.
-                          properties:
-                            policy:
-                              default: Isolated
-                              description: Policy specifies the network policy under
-                                which the Composition Function will run. Defaults
-                                to 'Isolated' - i.e. no network access. Specify 'Runner'
-                                to allow the function the same network access as its
-                                runner.
-                              enum:
-                              - Isolated
-                              - Runner
-                              type: string
-                          type: object
-                        resources:
-                          description: Resources that may be used by the Composition
-                            Function.
-                          properties:
-                            limits:
-                              description: Limits specify the maximum compute resources
-                                that may be used by the Composition Function.
-                              properties:
-                                cpu:
-                                  anyOf:
-                                  - type: integer
-                                  - type: string
-                                  default: 100m
-                                  description: CPU, in cores. (500m = .5 cores)
-                                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                                  x-kubernetes-int-or-string: true
-                                memory:
-                                  anyOf:
-                                  - type: integer
-                                  - type: string
-                                  default: 128Mi
-                                  description: Memory, in bytes. (500Gi = 500GiB =
-                                    500 * 1024 * 1024 * 1024)
-                                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                                  x-kubernetes-int-or-string: true
-                              type: object
-                          type: object
-                        runner:
-                          description: Runner configuration for the Composition Function.
-                          properties:
-                            endpoint:
-                              default: unix-abstract:crossplane/fn/default.sock
-                              description: Endpoint specifies how and where Crossplane
-                                should reach the runner it uses to invoke containerized
-                                Composition Functions.
-                              type: string
-                          type: object
-                        timeout:
-                          default: 20s
-                          description: Timeout after which the Composition Function
-                            will be killed.
-                          type: string
-                      required:
-                      - image
-                      type: object
-                    name:
-                      description: Name of this function. Must be unique within its
-                        Composition.
-                      type: string
-                    type:
-                      description: Type of this function.
-                      enum:
-                      - Container
-                      type: string
-                  required:
-                  - name
-                  - type
-                  type: object
-                type: array
+              mode:
+                default: Resources
+                description: "Mode controls what type or \"mode\" of Composition will
+                  be used. \n \"Resources\" (the default) indicates that a Composition
+                  uses what is commonly referred to as \"Patch & Transform\" or P&T
+                  composition. This mode of Composition uses an array of resources,
+                  each a template for a composed resource. \n \"Pipeline\" indicates
+                  that a Composition specifies a pipeline of Composition Functions,
+                  each of which is responsible for producing composed resources that
+                  Crossplane should create or update. THE PIPELINE MODE IS A BETA
+                  FEATURE. It is not honored if the relevant Crossplane feature flag
+                  is disabled."
+                enum:
+                - Resources
+                - Pipeline
+                type: string
               patchSets:
-                description: PatchSets define a named set of patches that may be included
-                  by any resource in this Composition. PatchSets cannot themselves
-                  refer to other PatchSets.
+                description: "PatchSets define a named set of patches that may be
+                  included by any resource in this Composition. PatchSets cannot themselves
+                  refer to other PatchSets. \n PatchSets are only used by the \"Resources\"
+                  mode of Composition. They are ignored by other modes."
                 items:
                   description: A PatchSet is a set of patches that can be reused from
                     all resources within a Composition.
@@ -995,13 +889,51 @@ spec:
                   - patches
                   type: object
                 type: array
+              pipeline:
+                description: "Pipeline is a list of composition function steps that
+                  will be used when a composite resource referring to this composition
+                  is created. One of resources and pipeline must be specified - you
+                  cannot specify both. \n The Pipeline is only used by the \"Pipeline\"
+                  mode of Composition. It is ignored by other modes. \n THIS IS A
+                  BETA FIELD. It is not honored if the relevant Crossplane feature
+                  flag is disabled."
+                items:
+                  description: A PipelineStep in a Composition Function pipeline.
+                  properties:
+                    functionRef:
+                      description: FunctionRef is a reference to the Composition Function
+                        this step should execute.
+                      properties:
+                        name:
+                          description: Name of the referenced Function.
+                          type: string
+                      required:
+                      - name
+                      type: object
+                    input:
+                      description: Input is an optional, arbitrary Kubernetes resource
+                        (i.e. a resource with an apiVersion and kind) that will be
+                        passed to the Composition Function as the 'input' of its RunFunctionRequest.
+                      type: object
+                      x-kubernetes-embedded-resource: true
+                      x-kubernetes-preserve-unknown-fields: true
+                    step:
+                      description: Step name. Must be unique within its Pipeline.
+                      type: string
+                  required:
+                  - functionRef
+                  - step
+                  type: object
+                type: array
               publishConnectionDetailsWithStoreConfigRef:
                 default:
                   name: default
-                description: PublishConnectionDetailsWithStoreConfig specifies the
+                description: "PublishConnectionDetailsWithStoreConfig specifies the
                   secret store config with which the connection details of composite
                   resources dynamically provisioned using this composition will be
-                  published.
+                  published. \n THIS IS AN ALPHA FIELD. Do not use it in production.
+                  It is not honored unless the relevant Crossplane feature flag is
+                  enabled, and may be changed or removed without notice."
                 properties:
                   name:
                     description: Name of the referenced StoreConfig.
@@ -1010,9 +942,10 @@ spec:
                 - name
                 type: object
               resources:
-                description: Resources is the list of resource templates that will
+                description: "Resources is a list of resource templates that will
                   be used when a composite resource referring to this composition
-                  is created.
+                  is created. \n Resources are only used by the \"Resources\" mode
+                  of Composition. They are ignored by other modes."
                 items:
                   description: ComposedTemplate is used to provide information about
                     how the composed resource should be processed.
@@ -1483,7 +1416,7 @@ spec:
                 description: WriteConnectionSecretsToNamespace specifies the namespace
                   in which the connection secrets of composite resource dynamically
                   provisioned using this composition will be created. This field is
-                  planned to be removed in a future release in favor of PublishConnectionDetailsWithStoreConfigRef.
+                  planned to be replaced in a future release in favor of PublishConnectionDetailsWithStoreConfigRef.
                   Currently, both could be set independently and connection details
                   would be published to both without affecting each other as long
                   as related fields at MR level specified.
@@ -1585,8 +1518,10 @@ spec:
                 - kind
                 type: object
               environment:
-                description: Environment configures the environment in which resources
-                  are rendered.
+                description: "Environment configures the environment in which resources
+                  are rendered. \n THIS IS AN ALPHA FIELD. Do not use it in production.
+                  It is not honored unless the relevant Crossplane feature flag is
+                  enabled, and may be changed or removed without notice."
                 properties:
                   defaultData:
                     additionalProperties:
@@ -2040,135 +1975,27 @@ spec:
                         type: string
                     type: object
                 type: object
-              functions:
-                description: Functions is list of Composition Functions that will
-                  be used when a composite resource referring to this composition
-                  is created. At least one of resources and functions must be specified.
-                  If both are specified the resources will be rendered first, then
-                  passed to the functions for further processing.
-                items:
-                  description: A Function represents a Composition Function.
-                  properties:
-                    config:
-                      description: Config is an optional, arbitrary Kubernetes resource
-                        (i.e. a resource with an apiVersion and kind) that will be
-                        passed to the Composition Function as the 'config' block of
-                        its FunctionIO.
-                      type: object
-                      x-kubernetes-embedded-resource: true
-                      x-kubernetes-preserve-unknown-fields: true
-                    container:
-                      description: Container configuration of this function.
-                      properties:
-                        image:
-                          description: Image specifies the OCI image in which the
-                            function is packaged. The image should include an entrypoint
-                            that reads a FunctionIO from stdin and emits it, optionally
-                            mutated, to stdout.
-                          type: string
-                        imagePullPolicy:
-                          default: IfNotPresent
-                          description: ImagePullPolicy defines the pull policy for
-                            the function image.
-                          enum:
-                          - IfNotPresent
-                          - Always
-                          - Never
-                          type: string
-                        imagePullSecrets:
-                          description: ImagePullSecrets are used to pull images from
-                            private OCI registries.
-                          items:
-                            description: LocalObjectReference contains enough information
-                              to let you locate the referenced object inside the same
-                              namespace.
-                            properties:
-                              name:
-                                description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-                                  TODO: Add other useful fields. apiVersion, kind,
-                                  uid?'
-                                type: string
-                            type: object
-                            x-kubernetes-map-type: atomic
-                          type: array
-                        network:
-                          description: Network configuration for the Composition Function.
-                          properties:
-                            policy:
-                              default: Isolated
-                              description: Policy specifies the network policy under
-                                which the Composition Function will run. Defaults
-                                to 'Isolated' - i.e. no network access. Specify 'Runner'
-                                to allow the function the same network access as its
-                                runner.
-                              enum:
-                              - Isolated
-                              - Runner
-                              type: string
-                          type: object
-                        resources:
-                          description: Resources that may be used by the Composition
-                            Function.
-                          properties:
-                            limits:
-                              description: Limits specify the maximum compute resources
-                                that may be used by the Composition Function.
-                              properties:
-                                cpu:
-                                  anyOf:
-                                  - type: integer
-                                  - type: string
-                                  default: 100m
-                                  description: CPU, in cores. (500m = .5 cores)
-                                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                                  x-kubernetes-int-or-string: true
-                                memory:
-                                  anyOf:
-                                  - type: integer
-                                  - type: string
-                                  default: 128Mi
-                                  description: Memory, in bytes. (500Gi = 500GiB =
-                                    500 * 1024 * 1024 * 1024)
-                                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                                  x-kubernetes-int-or-string: true
-                              type: object
-                          type: object
-                        runner:
-                          description: Runner configuration for the Composition Function.
-                          properties:
-                            endpoint:
-                              default: unix-abstract:crossplane/fn/default.sock
-                              description: Endpoint specifies how and where Crossplane
-                                should reach the runner it uses to invoke containerized
-                                Composition Functions.
-                              type: string
-                          type: object
-                        timeout:
-                          default: 20s
-                          description: Timeout after which the Composition Function
-                            will be killed.
-                          type: string
-                      required:
-                      - image
-                      type: object
-                    name:
-                      description: Name of this function. Must be unique within its
-                        Composition.
-                      type: string
-                    type:
-                      description: Type of this function.
-                      enum:
-                      - Container
-                      type: string
-                  required:
-                  - name
-                  - type
-                  type: object
-                type: array
+              mode:
+                default: Resources
+                description: "Mode controls what type or \"mode\" of Composition will
+                  be used. \n \"Resources\" (the default) indicates that a Composition
+                  uses what is commonly referred to as \"Patch & Transform\" or P&T
+                  composition. This mode of Composition uses an array of resources,
+                  each a template for a composed resource. \n \"Pipeline\" indicates
+                  that a Composition specifies a pipeline of Composition Functions,
+                  each of which is responsible for producing composed resources that
+                  Crossplane should create or update. THE PIPELINE MODE IS A BETA
+                  FEATURE. It is not honored if the relevant Crossplane feature flag
+                  is disabled."
+                enum:
+                - Resources
+                - Pipeline
+                type: string
               patchSets:
-                description: PatchSets define a named set of patches that may be included
-                  by any resource in this Composition. PatchSets cannot themselves
-                  refer to other PatchSets.
+                description: "PatchSets define a named set of patches that may be
+                  included by any resource in this Composition. PatchSets cannot themselves
+                  refer to other PatchSets. \n PatchSets are only used by the \"Resources\"
+                  mode of Composition. They are ignored by other modes."
                 items:
                   description: A PatchSet is a set of patches that can be reused from
                     all resources within a Composition.
@@ -2513,13 +2340,51 @@ spec:
                   - patches
                   type: object
                 type: array
+              pipeline:
+                description: "Pipeline is a list of composition function steps that
+                  will be used when a composite resource referring to this composition
+                  is created. One of resources and pipeline must be specified - you
+                  cannot specify both. \n The Pipeline is only used by the \"Pipeline\"
+                  mode of Composition. It is ignored by other modes. \n THIS IS A
+                  BETA FIELD. It is not honored if the relevant Crossplane feature
+                  flag is disabled."
+                items:
+                  description: A PipelineStep in a Composition Function pipeline.
+                  properties:
+                    functionRef:
+                      description: FunctionRef is a reference to the Composition Function
+                        this step should execute.
+                      properties:
+                        name:
+                          description: Name of the referenced Function.
+                          type: string
+                      required:
+                      - name
+                      type: object
+                    input:
+                      description: Input is an optional, arbitrary Kubernetes resource
+                        (i.e. a resource with an apiVersion and kind) that will be
+                        passed to the Composition Function as the 'input' of its RunFunctionRequest.
+                      type: object
+                      x-kubernetes-embedded-resource: true
+                      x-kubernetes-preserve-unknown-fields: true
+                    step:
+                      description: Step name. Must be unique within its Pipeline.
+                      type: string
+                  required:
+                  - functionRef
+                  - step
+                  type: object
+                type: array
               publishConnectionDetailsWithStoreConfigRef:
                 default:
                   name: default
-                description: PublishConnectionDetailsWithStoreConfig specifies the
+                description: "PublishConnectionDetailsWithStoreConfig specifies the
                   secret store config with which the connection details of composite
                   resources dynamically provisioned using this composition will be
-                  published.
+                  published. \n THIS IS AN ALPHA FIELD. Do not use it in production.
+                  It is not honored unless the relevant Crossplane feature flag is
+                  enabled, and may be changed or removed without notice."
                 properties:
                   name:
                     description: Name of the referenced StoreConfig.
@@ -2528,9 +2393,10 @@ spec:
                 - name
                 type: object
               resources:
-                description: Resources is the list of resource templates that will
+                description: "Resources is a list of resource templates that will
                   be used when a composite resource referring to this composition
-                  is created.
+                  is created. \n Resources are only used by the \"Resources\" mode
+                  of Composition. They are ignored by other modes."
                 items:
                   description: ComposedTemplate is used to provide information about
                     how the composed resource should be processed.
@@ -3001,7 +2867,7 @@ spec:
                 description: WriteConnectionSecretsToNamespace specifies the namespace
                   in which the connection secrets of composite resource dynamically
                   provisioned using this composition will be created. This field is
-                  planned to be removed in a future release in favor of PublishConnectionDetailsWithStoreConfigRef.
+                  planned to be replaced in a future release in favor of PublishConnectionDetailsWithStoreConfigRef.
                   Currently, both could be set independently and connection details
                   would be published to both without affecting each other as long
                   as related fields at MR level specified.
diff --git a/cluster/crds/apiextensions.crossplane.io_compositions.yaml b/cluster/crds/apiextensions.crossplane.io_compositions.yaml
index 138312ae7..70b4d1a43 100644
--- a/cluster/crds/apiextensions.crossplane.io_compositions.yaml
+++ b/cluster/crds/apiextensions.crossplane.io_compositions.yaml
@@ -62,10 +62,10 @@ spec:
                 - kind
                 type: object
               environment:
-                description: Environment configures the environment in which resources
-                  are rendered. THIS IS AN ALPHA FIELD. Do not use it in production.
+                description: "Environment configures the environment in which resources
+                  are rendered. \n THIS IS AN ALPHA FIELD. Do not use it in production.
                   It is not honored unless the relevant Crossplane feature flag is
-                  enabled, and may be changed or removed without notice.
+                  enabled, and may be changed or removed without notice."
                 properties:
                   defaultData:
                     additionalProperties:
@@ -519,138 +519,27 @@ spec:
                         type: string
                     type: object
                 type: object
-              functions:
-                description: "Functions is list of Composition Functions that will
-                  be used when a composite resource referring to this composition
-                  is created. At least one of resources and functions must be specified.
-                  If both are specified the resources will be rendered first, then
-                  passed to the functions for further processing. \n THIS IS AN ALPHA
-                  FIELD. Do not use it in production. It is not honored unless the
-                  relevant Crossplane feature flag is enabled, and may be changed
-                  or removed without notice."
-                items:
-                  description: A Function represents a Composition Function.
-                  properties:
-                    config:
-                      description: Config is an optional, arbitrary Kubernetes resource
-                        (i.e. a resource with an apiVersion and kind) that will be
-                        passed to the Composition Function as the 'config' block of
-                        its FunctionIO.
-                      type: object
-                      x-kubernetes-embedded-resource: true
-                      x-kubernetes-preserve-unknown-fields: true
-                    container:
-                      description: Container configuration of this function.
-                      properties:
-                        image:
-                          description: Image specifies the OCI image in which the
-                            function is packaged. The image should include an entrypoint
-                            that reads a FunctionIO from stdin and emits it, optionally
-                            mutated, to stdout.
-                          type: string
-                        imagePullPolicy:
-                          default: IfNotPresent
-                          description: ImagePullPolicy defines the pull policy for
-                            the function image.
-                          enum:
-                          - IfNotPresent
-                          - Always
-                          - Never
-                          type: string
-                        imagePullSecrets:
-                          description: ImagePullSecrets are used to pull images from
-                            private OCI registries.
-                          items:
-                            description: LocalObjectReference contains enough information
-                              to let you locate the referenced object inside the same
-                              namespace.
-                            properties:
-                              name:
-                                description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-                                  TODO: Add other useful fields. apiVersion, kind,
-                                  uid?'
-                                type: string
-                            type: object
-                            x-kubernetes-map-type: atomic
-                          type: array
-                        network:
-                          description: Network configuration for the Composition Function.
-                          properties:
-                            policy:
-                              default: Isolated
-                              description: Policy specifies the network policy under
-                                which the Composition Function will run. Defaults
-                                to 'Isolated' - i.e. no network access. Specify 'Runner'
-                                to allow the function the same network access as its
-                                runner.
-                              enum:
-                              - Isolated
-                              - Runner
-                              type: string
-                          type: object
-                        resources:
-                          description: Resources that may be used by the Composition
-                            Function.
-                          properties:
-                            limits:
-                              description: Limits specify the maximum compute resources
-                                that may be used by the Composition Function.
-                              properties:
-                                cpu:
-                                  anyOf:
-                                  - type: integer
-                                  - type: string
-                                  default: 100m
-                                  description: CPU, in cores. (500m = .5 cores)
-                                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                                  x-kubernetes-int-or-string: true
-                                memory:
-                                  anyOf:
-                                  - type: integer
-                                  - type: string
-                                  default: 128Mi
-                                  description: Memory, in bytes. (500Gi = 500GiB =
-                                    500 * 1024 * 1024 * 1024)
-                                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                                  x-kubernetes-int-or-string: true
-                              type: object
-                          type: object
-                        runner:
-                          description: Runner configuration for the Composition Function.
-                          properties:
-                            endpoint:
-                              default: unix-abstract:crossplane/fn/default.sock
-                              description: Endpoint specifies how and where Crossplane
-                                should reach the runner it uses to invoke containerized
-                                Composition Functions.
-                              type: string
-                          type: object
-                        timeout:
-                          default: 20s
-                          description: Timeout after which the Composition Function
-                            will be killed.
-                          type: string
-                      required:
-                      - image
-                      type: object
-                    name:
-                      description: Name of this function. Must be unique within its
-                        Composition.
-                      type: string
-                    type:
-                      description: Type of this function.
-                      enum:
-                      - Container
-                      type: string
-                  required:
-                  - name
-                  - type
-                  type: object
-                type: array
+              mode:
+                default: Resources
+                description: "Mode controls what type or \"mode\" of Composition will
+                  be used. \n \"Resources\" (the default) indicates that a Composition
+                  uses what is commonly referred to as \"Patch & Transform\" or P&T
+                  composition. This mode of Composition uses an array of resources,
+                  each a template for a composed resource. \n \"Pipeline\" indicates
+                  that a Composition specifies a pipeline of Composition Functions,
+                  each of which is responsible for producing composed resources that
+                  Crossplane should create or update. THE PIPELINE MODE IS A BETA
+                  FEATURE. It is not honored if the relevant Crossplane feature flag
+                  is disabled."
+                enum:
+                - Resources
+                - Pipeline
+                type: string
               patchSets:
-                description: PatchSets define a named set of patches that may be included
-                  by any resource in this Composition. PatchSets cannot themselves
-                  refer to other PatchSets.
+                description: "PatchSets define a named set of patches that may be
+                  included by any resource in this Composition. PatchSets cannot themselves
+                  refer to other PatchSets. \n PatchSets are only used by the \"Resources\"
+                  mode of Composition. They are ignored by other modes."
                 items:
                   description: A PatchSet is a set of patches that can be reused from
                     all resources within a Composition.
@@ -995,6 +884,42 @@ spec:
                   - patches
                   type: object
                 type: array
+              pipeline:
+                description: "Pipeline is a list of composition function steps that
+                  will be used when a composite resource referring to this composition
+                  is created. One of resources and pipeline must be specified - you
+                  cannot specify both. \n The Pipeline is only used by the \"Pipeline\"
+                  mode of Composition. It is ignored by other modes. \n THIS IS A
+                  BETA FIELD. It is not honored if the relevant Crossplane feature
+                  flag is disabled."
+                items:
+                  description: A PipelineStep in a Composition Function pipeline.
+                  properties:
+                    functionRef:
+                      description: FunctionRef is a reference to the Composition Function
+                        this step should execute.
+                      properties:
+                        name:
+                          description: Name of the referenced Function.
+                          type: string
+                      required:
+                      - name
+                      type: object
+                    input:
+                      description: Input is an optional, arbitrary Kubernetes resource
+                        (i.e. a resource with an apiVersion and kind) that will be
+                        passed to the Composition Function as the 'input' of its RunFunctionRequest.
+                      type: object
+                      x-kubernetes-embedded-resource: true
+                      x-kubernetes-preserve-unknown-fields: true
+                    step:
+                      description: Step name. Must be unique within its Pipeline.
+                      type: string
+                  required:
+                  - functionRef
+                  - step
+                  type: object
+                type: array
               publishConnectionDetailsWithStoreConfigRef:
                 default:
                   name: default
@@ -1012,11 +937,10 @@ spec:
                 - name
                 type: object
               resources:
-                description: Resources is a list of resource templates that will be
-                  used when a composite resource referring to this composition is
-                  created. At least one of resources and functions must be specififed.
-                  If both are specified the resources will be rendered first, then
-                  passed to the functions for further processing.
+                description: "Resources is a list of resource templates that will
+                  be used when a composite resource referring to this composition
+                  is created. \n Resources are only used by the \"Resources\" mode
+                  of Composition. They are ignored by other modes."
                 items:
                   description: ComposedTemplate is used to provide information about
                     how the composed resource should be processed.
diff --git a/cluster/crds/pkg.crossplane.io_functionrevisions.yaml b/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
index f66ea6fc1..52af42a30 100644
--- a/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
+++ b/cluster/crds/pkg.crossplane.io_functionrevisions.yaml
@@ -38,7 +38,7 @@ spec:
     - jsonPath: .metadata.creationTimestamp
       name: AGE
       type: date
-    name: v1alpha1
+    name: v1beta1
     schema:
       openAPIV3Schema:
         description: A FunctionRevision that has been added to Crossplane.
diff --git a/cluster/crds/pkg.crossplane.io_functions.yaml b/cluster/crds/pkg.crossplane.io_functions.yaml
index 633b0faaa..0a20cbf8a 100644
--- a/cluster/crds/pkg.crossplane.io_functions.yaml
+++ b/cluster/crds/pkg.crossplane.io_functions.yaml
@@ -29,7 +29,7 @@ spec:
     - jsonPath: .metadata.creationTimestamp
       name: AGE
       type: date
-    name: v1alpha1
+    name: v1beta1
     schema:
       openAPIV3Schema:
         description: Function is the CRD type for a request to deploy a long-running
@@ -156,10 +156,6 @@ spec:
                   It will reflect the most up to date revision, whether it has been
                   activated or not.
                 type: string
-              endpoint:
-                description: Endpoint is the gRPC endpoint where Crossplane will send
-                  RunFunctionRequests.
-                type: string
             type: object
         type: object
     served: true
diff --git a/cluster/meta/meta.pkg.crossplane.io_functions.yaml b/cluster/meta/meta.pkg.crossplane.io_functions.yaml
index e8e1edb32..a7da90973 100644
--- a/cluster/meta/meta.pkg.crossplane.io_functions.yaml
+++ b/cluster/meta/meta.pkg.crossplane.io_functions.yaml
@@ -14,7 +14,7 @@ spec:
     singular: function
   scope: Namespaced
   versions:
-  - name: v1alpha1
+  - name: v1beta1
     schema:
       openAPIV3Schema:
         description: A Function is the description of a Crossplane Function package.
diff --git a/cmd/crossplane/core/core.go b/cmd/crossplane/core/core.go
index a5883f67b..835c480e4 100644
--- a/cmd/crossplane/core/core.go
+++ b/cmd/crossplane/core/core.go
@@ -18,6 +18,7 @@ limitations under the License.
 package core
 
 import (
+	"context"
 	"crypto/tls"
 	"net/http"
 	"net/http/pprof"
@@ -53,6 +54,7 @@ import (
 	"github.com/crossplane/crossplane/internal/usage"
 	"github.com/crossplane/crossplane/internal/validation/apiextensions/v1/composition"
 	"github.com/crossplane/crossplane/internal/validation/apiextensions/v1/xrd"
+	"github.com/crossplane/crossplane/internal/xfn"
 	"github.com/crossplane/crossplane/internal/xpkg"
 )
 
@@ -81,32 +83,34 @@ func (c *Command) Run() error {
 type startCommand struct {
 	Profile string `placeholder:"host:port" help:"Serve runtime profiling data via HTTP at /debug/pprof."`
 
-	Namespace            string `short:"n" help:"Namespace used to unpack and run packages." default:"crossplane-system" env:"POD_NAMESPACE"`
-	ServiceAccount       string `help:"Name of the Crossplane Service Account." default:"crossplane" env:"POD_SERVICE_ACCOUNT"`
-	CacheDir             string `short:"c" help:"Directory used for caching package images." default:"/cache" env:"CACHE_DIR"`
-	LeaderElection       bool   `short:"l" help:"Use leader election for the controller manager." default:"false" env:"LEADER_ELECTION"`
-	Registry             string `short:"r" help:"Default registry used to fetch packages when not specified in tag." default:"${default_registry}" env:"REGISTRY"`
-	CABundlePath         string `help:"Additional CA bundle to use when fetching packages from registry." env:"CA_BUNDLE_PATH"`
+	Namespace      string `short:"n" help:"Namespace used to unpack and run packages." default:"crossplane-system" env:"POD_NAMESPACE"`
+	ServiceAccount string `help:"Name of the Crossplane Service Account." default:"crossplane" env:"POD_SERVICE_ACCOUNT"`
+	CacheDir       string `short:"c" help:"Directory used for caching package images." default:"/cache" env:"CACHE_DIR"`
+	LeaderElection bool   `short:"l" help:"Use leader election for the controller manager." default:"false" env:"LEADER_ELECTION"`
+	Registry       string `short:"r" help:"Default registry used to fetch packages when not specified in tag." default:"${default_registry}" env:"REGISTRY"`
+	CABundlePath   string `help:"Additional CA bundle to use when fetching packages from registry." env:"CA_BUNDLE_PATH"`
+	UserAgent      string `help:"The User-Agent header that will be set on all package requests." default:"${default_user_agent}" env:"USER_AGENT"`
+
+	SyncInterval     time.Duration `short:"s" help:"How often all resources will be double-checked for drift from the desired state." default:"1h"`
+	PollInterval     time.Duration `help:"How often individual resources will be checked for drift from the desired state." default:"1m"`
+	MaxReconcileRate int           `help:"The global maximum rate per second at which resources may checked for drift from the desired state." default:"10"`
+
+	ESSTLSSecretName     string `help:"The name of the TLS Secret that will be used by Crossplane and providers as clients of External Secret Store plugins." env:"ESS_TLS_SECRET_NAME"`
+	ESSTLSCertsDir       string `help:"The path of the folder which will store TLS certificates to be used by Crossplane and providers for communicating with External Secret Store plugins." env:"ESS_TLS_CERTS_DIR"`
 	WebhookTLSSecretName string `help:"The name of the TLS Secret that will be used by the webhook servers of core Crossplane and providers." env:"WEBHOOK_TLS_SECRET_NAME"`
 	WebhookTLSCertDir    string `help:"The directory of TLS certificate that will be used by the webhook server of core Crossplane. There should be tls.crt and tls.key files." env:"WEBHOOK_TLS_CERT_DIR"`
-	UserAgent            string `help:"The User-Agent header that will be set on all package requests." default:"${default_user_agent}" env:"USER_AGENT"`
-
-	SyncInterval        time.Duration `short:"s" help:"How often all resources will be double-checked for drift from the desired state." default:"1h"`
-	PollInterval        time.Duration `help:"How often individual resources will be checked for drift from the desired state." default:"1m"`
-	MaxReconcileRate    int           `help:"The global maximum rate per second at which resources may checked for drift from the desired state." default:"10"`
-	ESSTLSSecretName    string        `help:"The name of the TLS Secret that will be used by Crossplane and providers as clients of External Secret Store plugins." env:"ESS_TLS_SECRET_NAME"`
-	ESSTLSCertsDir      string        `help:"The path of the folder which will store TLS certificates to be used by Crossplane and providers for communicating with External Secret Store plugins." env:"ESS_TLS_CERTS_DIR"`
-	TLSServerSecretName string        `help:"The name of the TLS Secret that will store Crossplane's server certificate." env:"TLS_SERVER_SECRET_NAME"`
-	TLSServerCertsDir   string        `help:"The path of the folder which will store TLS server certificate of Crossplane." env:"TLS_SERVER_CERTS_DIR"`
-	TLSClientSecretName string        `help:"The name of the TLS Secret that will be store Crossplane's client certificate." env:"TLS_CLIENT_SECRET_NAME"`
-	TLSClientCertsDir   string        `help:"The path of the folder which will store TLS client certificate of Crossplane." env:"TLS_CLIENT_CERTS_DIR"`
+	TLSServerSecretName  string `help:"The name of the TLS Secret that will store Crossplane's server certificate." env:"TLS_SERVER_SECRET_NAME"`
+	TLSServerCertsDir    string `help:"The path of the folder which will store TLS server certificate of Crossplane." env:"TLS_SERVER_CERTS_DIR"`
+	TLSClientSecretName  string `help:"The name of the TLS Secret that will be store Crossplane's client certificate." env:"TLS_CLIENT_SECRET_NAME"`
+	TLSClientCertsDir    string `help:"The path of the folder which will store TLS client certificate of Crossplane." env:"TLS_CLIENT_CERTS_DIR"`
 
 	EnableEnvironmentConfigs                 bool `group:"Alpha Features:" help:"Enable support for EnvironmentConfigs."`
 	EnableExternalSecretStores               bool `group:"Alpha Features:" help:"Enable support for External Secret Stores."`
-	EnableCompositionFunctions               bool `group:"Alpha Features:" help:"Enable support for Composition Functions."`
 	EnableCompositionWebhookSchemaValidation bool `group:"Alpha Features:" help:"Enable support for Composition validation using schemas."`
 	EnableUsages                             bool `group:"Alpha Features:" help:"Enable support for deletion ordering and resource protection with Usages."`
 
+	EnableCompositionFunctions bool `group:"Beta Features:" default:"true" help:"Enable support for Composition Functions."`
+
 	// These are GA features that previously had alpha or beta feature flags.
 	// You can't turn off a GA feature. We maintain the flags to avoid breaking
 	// folks who are passing them, but they do nothing. The flags are hidden so
@@ -184,70 +188,93 @@ func (c *startCommand) Run(s *runtime.Scheme, log logging.Logger) error { //noli
 		return errors.Wrap(err, "Cannot create manager")
 	}
 
-	feats := &feature.Flags{}
-	if c.EnableEnvironmentConfigs {
-		feats.Enable(features.EnableAlphaEnvironmentConfigs)
-		log.Info("Alpha feature enabled", "flag", features.EnableAlphaEnvironmentConfigs)
+	o := controller.Options{
+		Logger:                  log,
+		MaxConcurrentReconciles: c.MaxReconcileRate,
+		PollInterval:            c.PollInterval,
+		GlobalRateLimiter:       ratelimiter.NewGlobal(c.MaxReconcileRate),
+		Features:                &feature.Flags{},
+	}
+
+	ao := apiextensionscontroller.Options{
+		Options:        o,
+		Namespace:      c.Namespace,
+		ServiceAccount: c.ServiceAccount,
+		Registry:       c.Registry,
+	}
+
+	clienttls, err := certificates.LoadMTLSConfig(
+		filepath.Join(c.TLSClientCertsDir, initializer.SecretKeyCACert),
+		filepath.Join(c.TLSClientCertsDir, corev1.TLSCertKey),
+		filepath.Join(c.TLSClientCertsDir, corev1.TLSPrivateKeyKey),
+		false)
+	if err != nil {
+		return errors.Wrap(err, "cannot load client TLS certificates ")
+	}
+
+	if !c.EnableCompositionRevisions {
+		log.Info("CompositionRevisions feature is GA and cannot be disabled. The --enable-composition-revisions flag will be removed in a future release.")
 	}
 	if c.EnableCompositionFunctions {
-		feats.Enable(features.EnableAlphaCompositionFunctions)
-		log.Info("Alpha feature enabled", "flag", features.EnableAlphaCompositionFunctions)
+		o.Features.Enable(features.EnableBetaCompositionFunctions)
+		log.Info("Beta feature enabled", "flag", features.EnableBetaCompositionFunctions)
+
+		// We want all XR controllers to share the same gRPC clients.
+		runner := xfn.NewPackagedFunctionRunner(mgr.GetClient(),
+			xfn.WithLogger(log),
+			xfn.WithTLSConfig(clienttls))
+
+		// Periodically remove clients for Functions that no longer exist.
+		ctx, cancel := context.WithCancel(context.Background())
+		defer cancel()
+		go runner.GarbageCollectConnections(ctx, 10*time.Minute)
+
+		ao.FunctionRunner = runner
+	}
+	if c.EnableEnvironmentConfigs {
+		o.Features.Enable(features.EnableAlphaEnvironmentConfigs)
+		log.Info("Alpha feature enabled", "flag", features.EnableAlphaEnvironmentConfigs)
 	}
 	if c.EnableCompositionWebhookSchemaValidation {
-		feats.Enable(features.EnableAlphaCompositionWebhookSchemaValidation)
+		o.Features.Enable(features.EnableAlphaCompositionWebhookSchemaValidation)
 		log.Info("Alpha feature enabled", "flag", features.EnableAlphaCompositionWebhookSchemaValidation)
 	}
 	if c.EnableUsages {
-		feats.Enable(features.EnableAlphaUsages)
+		o.Features.Enable(features.EnableAlphaUsages)
 		log.Info("Alpha feature enabled", "flag", features.EnableAlphaUsages)
 	}
-	if !c.EnableCompositionRevisions {
-		log.Info("CompositionRevisions feature is GA and cannot be disabled. The --enable-composition-revisions flag will be removed in a future release.")
-	}
-
-	o := controller.Options{
-		Logger:                  log,
-		MaxConcurrentReconciles: c.MaxReconcileRate,
-		PollInterval:            c.PollInterval,
-		GlobalRateLimiter:       ratelimiter.NewGlobal(c.MaxReconcileRate),
-		Features:                feats,
-	}
-
 	if c.EnableExternalSecretStores {
-		feats.Enable(features.EnableAlphaExternalSecretStores)
+		o.Features.Enable(features.EnableAlphaExternalSecretStores)
 		log.Info("Alpha feature enabled", "flag", features.EnableAlphaExternalSecretStores)
 
-		tlsConfig, err := certificates.LoadMTLSConfig(filepath.Join(c.ESSTLSCertsDir, initializer.SecretKeyCACert),
-			filepath.Join(c.ESSTLSCertsDir, corev1.TLSCertKey), filepath.Join(c.ESSTLSCertsDir, corev1.TLSPrivateKeyKey), false)
+		tcfg, err := certificates.LoadMTLSConfig(
+			filepath.Join(c.ESSTLSCertsDir, initializer.SecretKeyCACert),
+			filepath.Join(c.ESSTLSCertsDir, corev1.TLSCertKey),
+			filepath.Join(c.ESSTLSCertsDir, corev1.TLSPrivateKeyKey),
+			false)
 		if err != nil {
-			return errors.Wrap(err, "Cannot load TLS certificates for ESS")
+			return errors.Wrap(err, "Cannot load TLS certificates for external secret stores")
 		}
 
 		o.ESSOptions = &controller.ESSOptions{
-			TLSConfig:     tlsConfig,
+			TLSConfig:     tcfg,
 			TLSSecretName: &c.ESSTLSSecretName,
 		}
 	}
 
-	ao := apiextensionscontroller.Options{
-		Options:        o,
-		Namespace:      c.Namespace,
-		ServiceAccount: c.ServiceAccount,
-		Registry:       c.Registry,
-	}
-
 	if err := apiextensions.Setup(mgr, ao); err != nil {
 		return errors.Wrap(err, "Cannot setup API extension controllers")
 	}
 
 	po := pkgcontroller.Options{
-		Options:              o,
-		Cache:                xpkg.NewFsPackageCache(c.CacheDir, afero.NewOsFs()),
-		Namespace:            c.Namespace,
-		ServiceAccount:       c.ServiceAccount,
-		DefaultRegistry:      c.Registry,
-		Features:             feats,
-		FetcherOptions:       []xpkg.FetcherOpt{xpkg.WithUserAgent(c.UserAgent)},
+		Options:         o,
+		Cache:           xpkg.NewFsPackageCache(c.CacheDir, afero.NewOsFs()),
+		Namespace:       c.Namespace,
+		ServiceAccount:  c.ServiceAccount,
+		DefaultRegistry: c.Registry,
+		FetcherOptions:  []xpkg.FetcherOpt{xpkg.WithUserAgent(c.UserAgent)},
+
+		// TODO(negz): I think this should be ServerTLSSecretName now.
 		WebhookTLSSecretName: c.WebhookTLSSecretName,
 	}
 
diff --git a/design/design-doc-composition-functions.md b/design/design-doc-composition-functions.md
index e6a84e6ce..45000f1c3 100644
--- a/design/design-doc-composition-functions.md
+++ b/design/design-doc-composition-functions.md
@@ -185,14 +185,20 @@ The proposal put forward by this document should:
 
 ## Proposal
 
-This document proposes that a new `pipeline` array of Function calls be added to
-the existing `Composition` type. This array of Functions would be called either
-instead of or in addition to the existing `resources` array in order to
-determine how an XR should be composed. The array of Functions acts as a
+This document proposes that a new `mode` field be added to the existing
+`Composition` type. A Composition in `mode: Pipeline` must specify a `pipeline`
+- an array of Function calls. This array of Functions would be called in order
+to determine how an XR should be composed. The array of Functions acts as a
 pipeline; the output of each Function is passed as the input to the next. The
 output of the final Function call tells Crossplane what must be done to
 reconcile the XR.
 
+When in `mode: Pipeline` the `resources` and `patchSets` fields of a
+Composition's spec are ignored. Put otherwise, a Composition must specify
+P&T-style `resources`, or a `pipeline`. It cannot specify both. The new `mode`
+field must be optional. The default value is `mode: Resources`, which honors
+(only) the P&T `resources` as do GA Compositions today.
+
 ```yaml
 apiVersion: apiextensions.crossplane.io/v2alpha1
 kind: Composition
@@ -203,6 +209,7 @@ spec:
     apiVersion: database.example.org/v1
     kind: XPostgreSQLInstance
   # This Composition uses a pipeline of Functions instead of (P&T) resources.
+  mode: Pipeline
   pipeline:
     # Each step in the pipeline calls one Composition Function.
   - step: compose-xr-using-go-templates
@@ -233,8 +240,8 @@ Notably the Functions would not need to be responsible for interacting with the
 API server to create, update, or delete composed resources. Instead, they
 instruct Crossplane which resources should be created, updated, or deleted.
 Under the proposed design Functions could also be used for purposes besides
-rendering composed resources, for example validating the results of the
-`resources` array or earlier Functions in the `pipeline`.
+rendering composed resources, for example validating the results of earlier
+Functions in the `pipeline`.
 
 Before you can use a Function, you must install it. Installing a Function works
 just like installing a Provider:
@@ -288,9 +295,8 @@ message RunFunctionRequest {
   State observed = 2;
 
   // Desired state according to a Function pipeline. The state passed to a
-  // particular Function may have been accumulated by processing a Composition's
-  // patch-and-transform resources array. It may also have been accumulated by
-  // previous Functions in the pipeline.
+  // particular Function may have been accumulated by previous Functions in the
+  // pipeline.
   State desired = 3;
 
   // Optional input specific to this Function invocation. A JSON representation
@@ -347,7 +353,39 @@ message Resource {
   google.protobuf.Struct resource = 1;
 
   // The resource's connection details.
+  // 
+  // * Crossplane will set this field in a RunFunctionRequest to communicate the
+  //   the observed connection details of a composite or composed resource.
+  //
+  // * A Function should set this field in a RunFunctionResponse to indicate the
+  //   desired connection details of the composite resource.
+  //
+  // * A Function should not set this field in a RunFunctionResponse to indicate
+  //   the desired connection details of a composed resource. This will be
+  //   ignored.
   map<string, bytes> connection_details = 2;
+
+  // Ready indicates whether the resource should be considered ready.
+  // 
+  // * Crossplane will never set this field in a RunFunctionRequest.
+  //
+  // * A Function should set this field to READY_TRUE in a RunFunctionResponse
+  //   to indicate that a desired composed resource is ready.
+  //
+  // * A Function should not set this field in a RunFunctionResponse to indicate
+  //   that the desired composite resource is ready. This will be ignored.
+  Ready ready = 3;
+}
+
+// Ready indicates whether a composed resource should be considered ready.
+enum Ready {
+  READY_UNSPECIFIED = 0;
+
+  // True means the composed resource has been observed to be ready.
+  READY_TRUE = 1;
+
+  // False means the composed resource has not been observed to be ready.
+  READY_FALSE = 2;
 }
 
 // A Result of running a Function.
@@ -400,9 +438,9 @@ Some key differences between the alpha `FunctionIO` and the proposed beta
 
 The package manager is responsible for creating a headless Kubernetes Service
 where each Function's Deployment can be reached. The address of the Service will
-be exposed as the `status.endpoint` of the Function resource. The Service must
-be headless in order for Crossplane's gRPC client to load-balance connections
-when there are multiple Function replicas.
+be exposed as the `status.endpoint` of the active FunctionRevision resource. The
+Service must be headless in order for Crossplane's gRPC client to load-balance
+connections when there are multiple Function replicas.
 
 Note that the fact this endpoint is powered by a Service is an implementation
 detail; it may be possible for Functions to be reached (and indeed deployed) by
@@ -813,6 +851,7 @@ spec:
   compositeTypeRef:
     apiVersion: database.example.org/v1
     kind: XPostgreSQLInstance
+  mode: Pipeline
   pipeline:
   - step: compose-xr-using-go-templates
     functionRef:
@@ -857,6 +896,7 @@ spec:
   compositeTypeRef:
     apiVersion: database.example.org/v1
     kind: XPostgreSQLInstance
+  mode: Pipeline
   pipeline:
   - step: compose-xr-using-go-templates
     functionRef:
@@ -1097,6 +1137,7 @@ spec:
   compositeTypeRef:
     apiVersion: database.example.org/v1alpha1
     kind: AcmeCoDatabase
+  mode: Pipeline
   pipeline:
   - step: patch-and-transform
     functionRef:
diff --git a/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_pkg_client.go b/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_pkg_client.go
index 29cbd0121..ffc844254 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_pkg_client.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_pkg_client.go
@@ -32,14 +32,6 @@ func (c *FakePkgV1alpha1) ControllerConfigs() v1alpha1.ControllerConfigInterface
 	return &FakeControllerConfigs{c}
 }
 
-func (c *FakePkgV1alpha1) Functions() v1alpha1.FunctionInterface {
-	return &FakeFunctions{c}
-}
-
-func (c *FakePkgV1alpha1) FunctionRevisions() v1alpha1.FunctionRevisionInterface {
-	return &FakeFunctionRevisions{c}
-}
-
 // RESTClient returns a RESTClient that is used to communicate
 // with API server by this client implementation.
 func (c *FakePkgV1alpha1) RESTClient() rest.Interface {
diff --git a/internal/client/clientset/versioned/typed/pkg/v1alpha1/generated_expansion.go b/internal/client/clientset/versioned/typed/pkg/v1alpha1/generated_expansion.go
index 7ffe4b2d4..f6fcd2b95 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1alpha1/generated_expansion.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1alpha1/generated_expansion.go
@@ -19,7 +19,3 @@ limitations under the License.
 package v1alpha1
 
 type ControllerConfigExpansion interface{}
-
-type FunctionExpansion interface{}
-
-type FunctionRevisionExpansion interface{}
diff --git a/internal/client/clientset/versioned/typed/pkg/v1alpha1/pkg_client.go b/internal/client/clientset/versioned/typed/pkg/v1alpha1/pkg_client.go
index f48314a0f..8679b8b28 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1alpha1/pkg_client.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1alpha1/pkg_client.go
@@ -29,8 +29,6 @@ import (
 type PkgV1alpha1Interface interface {
 	RESTClient() rest.Interface
 	ControllerConfigsGetter
-	FunctionsGetter
-	FunctionRevisionsGetter
 }
 
 // PkgV1alpha1Client is used to interact with features provided by the pkg.crossplane.io group.
@@ -42,14 +40,6 @@ func (c *PkgV1alpha1Client) ControllerConfigs() ControllerConfigInterface {
 	return newControllerConfigs(c)
 }
 
-func (c *PkgV1alpha1Client) Functions() FunctionInterface {
-	return newFunctions(c)
-}
-
-func (c *PkgV1alpha1Client) FunctionRevisions() FunctionRevisionInterface {
-	return newFunctionRevisions(c)
-}
-
 // NewForConfig creates a new PkgV1alpha1Client for the given config.
 // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
 // where httpClient was generated with rest.HTTPClientFor(c).
diff --git a/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_function.go b/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_function.go
similarity index 75%
rename from internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_function.go
rename to internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_function.go
index 460074ec8..918a16f12 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_function.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_function.go
@@ -21,7 +21,7 @@ package fake
 import (
 	"context"
 
-	v1alpha1 "github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	v1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	labels "k8s.io/apimachinery/pkg/labels"
 	types "k8s.io/apimachinery/pkg/types"
@@ -31,27 +31,27 @@ import (
 
 // FakeFunctions implements FunctionInterface
 type FakeFunctions struct {
-	Fake *FakePkgV1alpha1
+	Fake *FakePkgV1beta1
 }
 
-var functionsResource = v1alpha1.SchemeGroupVersion.WithResource("functions")
+var functionsResource = v1beta1.SchemeGroupVersion.WithResource("functions")
 
-var functionsKind = v1alpha1.SchemeGroupVersion.WithKind("Function")
+var functionsKind = v1beta1.SchemeGroupVersion.WithKind("Function")
 
 // Get takes name of the function, and returns the corresponding function object, and an error if there is any.
-func (c *FakeFunctions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Function, err error) {
+func (c *FakeFunctions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Function, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootGetAction(functionsResource, name), &v1alpha1.Function{})
+		Invokes(testing.NewRootGetAction(functionsResource, name), &v1beta1.Function{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.Function), err
+	return obj.(*v1beta1.Function), err
 }
 
 // List takes label and field selectors, and returns the list of Functions that match those selectors.
-func (c *FakeFunctions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.FunctionList, err error) {
+func (c *FakeFunctions) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.FunctionList, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootListAction(functionsResource, functionsKind, opts), &v1alpha1.FunctionList{})
+		Invokes(testing.NewRootListAction(functionsResource, functionsKind, opts), &v1beta1.FunctionList{})
 	if obj == nil {
 		return nil, err
 	}
@@ -60,8 +60,8 @@ func (c *FakeFunctions) List(ctx context.Context, opts v1.ListOptions) (result *
 	if label == nil {
 		label = labels.Everything()
 	}
-	list := &v1alpha1.FunctionList{ListMeta: obj.(*v1alpha1.FunctionList).ListMeta}
-	for _, item := range obj.(*v1alpha1.FunctionList).Items {
+	list := &v1beta1.FunctionList{ListMeta: obj.(*v1beta1.FunctionList).ListMeta}
+	for _, item := range obj.(*v1beta1.FunctionList).Items {
 		if label.Matches(labels.Set(item.Labels)) {
 			list.Items = append(list.Items, item)
 		}
@@ -76,40 +76,40 @@ func (c *FakeFunctions) Watch(ctx context.Context, opts v1.ListOptions) (watch.I
 }
 
 // Create takes the representation of a function and creates it.  Returns the server's representation of the function, and an error, if there is any.
-func (c *FakeFunctions) Create(ctx context.Context, function *v1alpha1.Function, opts v1.CreateOptions) (result *v1alpha1.Function, err error) {
+func (c *FakeFunctions) Create(ctx context.Context, function *v1beta1.Function, opts v1.CreateOptions) (result *v1beta1.Function, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootCreateAction(functionsResource, function), &v1alpha1.Function{})
+		Invokes(testing.NewRootCreateAction(functionsResource, function), &v1beta1.Function{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.Function), err
+	return obj.(*v1beta1.Function), err
 }
 
 // Update takes the representation of a function and updates it. Returns the server's representation of the function, and an error, if there is any.
-func (c *FakeFunctions) Update(ctx context.Context, function *v1alpha1.Function, opts v1.UpdateOptions) (result *v1alpha1.Function, err error) {
+func (c *FakeFunctions) Update(ctx context.Context, function *v1beta1.Function, opts v1.UpdateOptions) (result *v1beta1.Function, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootUpdateAction(functionsResource, function), &v1alpha1.Function{})
+		Invokes(testing.NewRootUpdateAction(functionsResource, function), &v1beta1.Function{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.Function), err
+	return obj.(*v1beta1.Function), err
 }
 
 // UpdateStatus was generated because the type contains a Status member.
 // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
-func (c *FakeFunctions) UpdateStatus(ctx context.Context, function *v1alpha1.Function, opts v1.UpdateOptions) (*v1alpha1.Function, error) {
+func (c *FakeFunctions) UpdateStatus(ctx context.Context, function *v1beta1.Function, opts v1.UpdateOptions) (*v1beta1.Function, error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootUpdateSubresourceAction(functionsResource, "status", function), &v1alpha1.Function{})
+		Invokes(testing.NewRootUpdateSubresourceAction(functionsResource, "status", function), &v1beta1.Function{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.Function), err
+	return obj.(*v1beta1.Function), err
 }
 
 // Delete takes name of the function and deletes it. Returns an error if one occurs.
 func (c *FakeFunctions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewRootDeleteActionWithOptions(functionsResource, name, opts), &v1alpha1.Function{})
+		Invokes(testing.NewRootDeleteActionWithOptions(functionsResource, name, opts), &v1beta1.Function{})
 	return err
 }
 
@@ -117,16 +117,16 @@ func (c *FakeFunctions) Delete(ctx context.Context, name string, opts v1.DeleteO
 func (c *FakeFunctions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
 	action := testing.NewRootDeleteCollectionAction(functionsResource, listOpts)
 
-	_, err := c.Fake.Invokes(action, &v1alpha1.FunctionList{})
+	_, err := c.Fake.Invokes(action, &v1beta1.FunctionList{})
 	return err
 }
 
 // Patch applies the patch and returns the patched function.
-func (c *FakeFunctions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Function, err error) {
+func (c *FakeFunctions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Function, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootPatchSubresourceAction(functionsResource, name, pt, data, subresources...), &v1alpha1.Function{})
+		Invokes(testing.NewRootPatchSubresourceAction(functionsResource, name, pt, data, subresources...), &v1beta1.Function{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.Function), err
+	return obj.(*v1beta1.Function), err
 }
diff --git a/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_functionrevision.go b/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_functionrevision.go
similarity index 70%
rename from internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_functionrevision.go
rename to internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_functionrevision.go
index 24b681eac..13ea5f5ec 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1alpha1/fake/fake_functionrevision.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_functionrevision.go
@@ -21,7 +21,7 @@ package fake
 import (
 	"context"
 
-	v1alpha1 "github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	v1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	labels "k8s.io/apimachinery/pkg/labels"
 	types "k8s.io/apimachinery/pkg/types"
@@ -31,27 +31,27 @@ import (
 
 // FakeFunctionRevisions implements FunctionRevisionInterface
 type FakeFunctionRevisions struct {
-	Fake *FakePkgV1alpha1
+	Fake *FakePkgV1beta1
 }
 
-var functionrevisionsResource = v1alpha1.SchemeGroupVersion.WithResource("functionrevisions")
+var functionrevisionsResource = v1beta1.SchemeGroupVersion.WithResource("functionrevisions")
 
-var functionrevisionsKind = v1alpha1.SchemeGroupVersion.WithKind("FunctionRevision")
+var functionrevisionsKind = v1beta1.SchemeGroupVersion.WithKind("FunctionRevision")
 
 // Get takes name of the functionRevision, and returns the corresponding functionRevision object, and an error if there is any.
-func (c *FakeFunctionRevisions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.FunctionRevision, err error) {
+func (c *FakeFunctionRevisions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.FunctionRevision, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootGetAction(functionrevisionsResource, name), &v1alpha1.FunctionRevision{})
+		Invokes(testing.NewRootGetAction(functionrevisionsResource, name), &v1beta1.FunctionRevision{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.FunctionRevision), err
+	return obj.(*v1beta1.FunctionRevision), err
 }
 
 // List takes label and field selectors, and returns the list of FunctionRevisions that match those selectors.
-func (c *FakeFunctionRevisions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.FunctionRevisionList, err error) {
+func (c *FakeFunctionRevisions) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.FunctionRevisionList, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootListAction(functionrevisionsResource, functionrevisionsKind, opts), &v1alpha1.FunctionRevisionList{})
+		Invokes(testing.NewRootListAction(functionrevisionsResource, functionrevisionsKind, opts), &v1beta1.FunctionRevisionList{})
 	if obj == nil {
 		return nil, err
 	}
@@ -60,8 +60,8 @@ func (c *FakeFunctionRevisions) List(ctx context.Context, opts v1.ListOptions) (
 	if label == nil {
 		label = labels.Everything()
 	}
-	list := &v1alpha1.FunctionRevisionList{ListMeta: obj.(*v1alpha1.FunctionRevisionList).ListMeta}
-	for _, item := range obj.(*v1alpha1.FunctionRevisionList).Items {
+	list := &v1beta1.FunctionRevisionList{ListMeta: obj.(*v1beta1.FunctionRevisionList).ListMeta}
+	for _, item := range obj.(*v1beta1.FunctionRevisionList).Items {
 		if label.Matches(labels.Set(item.Labels)) {
 			list.Items = append(list.Items, item)
 		}
@@ -76,40 +76,40 @@ func (c *FakeFunctionRevisions) Watch(ctx context.Context, opts v1.ListOptions)
 }
 
 // Create takes the representation of a functionRevision and creates it.  Returns the server's representation of the functionRevision, and an error, if there is any.
-func (c *FakeFunctionRevisions) Create(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.CreateOptions) (result *v1alpha1.FunctionRevision, err error) {
+func (c *FakeFunctionRevisions) Create(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.CreateOptions) (result *v1beta1.FunctionRevision, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootCreateAction(functionrevisionsResource, functionRevision), &v1alpha1.FunctionRevision{})
+		Invokes(testing.NewRootCreateAction(functionrevisionsResource, functionRevision), &v1beta1.FunctionRevision{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.FunctionRevision), err
+	return obj.(*v1beta1.FunctionRevision), err
 }
 
 // Update takes the representation of a functionRevision and updates it. Returns the server's representation of the functionRevision, and an error, if there is any.
-func (c *FakeFunctionRevisions) Update(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.UpdateOptions) (result *v1alpha1.FunctionRevision, err error) {
+func (c *FakeFunctionRevisions) Update(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.UpdateOptions) (result *v1beta1.FunctionRevision, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootUpdateAction(functionrevisionsResource, functionRevision), &v1alpha1.FunctionRevision{})
+		Invokes(testing.NewRootUpdateAction(functionrevisionsResource, functionRevision), &v1beta1.FunctionRevision{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.FunctionRevision), err
+	return obj.(*v1beta1.FunctionRevision), err
 }
 
 // UpdateStatus was generated because the type contains a Status member.
 // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
-func (c *FakeFunctionRevisions) UpdateStatus(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.UpdateOptions) (*v1alpha1.FunctionRevision, error) {
+func (c *FakeFunctionRevisions) UpdateStatus(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.UpdateOptions) (*v1beta1.FunctionRevision, error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootUpdateSubresourceAction(functionrevisionsResource, "status", functionRevision), &v1alpha1.FunctionRevision{})
+		Invokes(testing.NewRootUpdateSubresourceAction(functionrevisionsResource, "status", functionRevision), &v1beta1.FunctionRevision{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.FunctionRevision), err
+	return obj.(*v1beta1.FunctionRevision), err
 }
 
 // Delete takes name of the functionRevision and deletes it. Returns an error if one occurs.
 func (c *FakeFunctionRevisions) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
 	_, err := c.Fake.
-		Invokes(testing.NewRootDeleteActionWithOptions(functionrevisionsResource, name, opts), &v1alpha1.FunctionRevision{})
+		Invokes(testing.NewRootDeleteActionWithOptions(functionrevisionsResource, name, opts), &v1beta1.FunctionRevision{})
 	return err
 }
 
@@ -117,16 +117,16 @@ func (c *FakeFunctionRevisions) Delete(ctx context.Context, name string, opts v1
 func (c *FakeFunctionRevisions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
 	action := testing.NewRootDeleteCollectionAction(functionrevisionsResource, listOpts)
 
-	_, err := c.Fake.Invokes(action, &v1alpha1.FunctionRevisionList{})
+	_, err := c.Fake.Invokes(action, &v1beta1.FunctionRevisionList{})
 	return err
 }
 
 // Patch applies the patch and returns the patched functionRevision.
-func (c *FakeFunctionRevisions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.FunctionRevision, err error) {
+func (c *FakeFunctionRevisions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.FunctionRevision, err error) {
 	obj, err := c.Fake.
-		Invokes(testing.NewRootPatchSubresourceAction(functionrevisionsResource, name, pt, data, subresources...), &v1alpha1.FunctionRevision{})
+		Invokes(testing.NewRootPatchSubresourceAction(functionrevisionsResource, name, pt, data, subresources...), &v1beta1.FunctionRevision{})
 	if obj == nil {
 		return nil, err
 	}
-	return obj.(*v1alpha1.FunctionRevision), err
+	return obj.(*v1beta1.FunctionRevision), err
 }
diff --git a/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_pkg_client.go b/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_pkg_client.go
index f7497c381..c0fc87c32 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_pkg_client.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1beta1/fake/fake_pkg_client.go
@@ -28,6 +28,14 @@ type FakePkgV1beta1 struct {
 	*testing.Fake
 }
 
+func (c *FakePkgV1beta1) Functions() v1beta1.FunctionInterface {
+	return &FakeFunctions{c}
+}
+
+func (c *FakePkgV1beta1) FunctionRevisions() v1beta1.FunctionRevisionInterface {
+	return &FakeFunctionRevisions{c}
+}
+
 func (c *FakePkgV1beta1) Locks() v1beta1.LockInterface {
 	return &FakeLocks{c}
 }
diff --git a/internal/client/clientset/versioned/typed/pkg/v1alpha1/function.go b/internal/client/clientset/versioned/typed/pkg/v1beta1/function.go
similarity index 78%
rename from internal/client/clientset/versioned/typed/pkg/v1alpha1/function.go
rename to internal/client/clientset/versioned/typed/pkg/v1beta1/function.go
index 7b16c6e36..95a5d6dd4 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1alpha1/function.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1beta1/function.go
@@ -16,13 +16,13 @@ limitations under the License.
 
 // Code generated by client-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
 	"context"
 	"time"
 
-	v1alpha1 "github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	v1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	scheme "github.com/crossplane/crossplane/internal/client/clientset/versioned/scheme"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	types "k8s.io/apimachinery/pkg/types"
@@ -38,15 +38,15 @@ type FunctionsGetter interface {
 
 // FunctionInterface has methods to work with Function resources.
 type FunctionInterface interface {
-	Create(ctx context.Context, function *v1alpha1.Function, opts v1.CreateOptions) (*v1alpha1.Function, error)
-	Update(ctx context.Context, function *v1alpha1.Function, opts v1.UpdateOptions) (*v1alpha1.Function, error)
-	UpdateStatus(ctx context.Context, function *v1alpha1.Function, opts v1.UpdateOptions) (*v1alpha1.Function, error)
+	Create(ctx context.Context, function *v1beta1.Function, opts v1.CreateOptions) (*v1beta1.Function, error)
+	Update(ctx context.Context, function *v1beta1.Function, opts v1.UpdateOptions) (*v1beta1.Function, error)
+	UpdateStatus(ctx context.Context, function *v1beta1.Function, opts v1.UpdateOptions) (*v1beta1.Function, error)
 	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
 	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
-	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Function, error)
-	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.FunctionList, error)
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.Function, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta1.FunctionList, error)
 	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
-	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Function, err error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Function, err error)
 	FunctionExpansion
 }
 
@@ -56,15 +56,15 @@ type functions struct {
 }
 
 // newFunctions returns a Functions
-func newFunctions(c *PkgV1alpha1Client) *functions {
+func newFunctions(c *PkgV1beta1Client) *functions {
 	return &functions{
 		client: c.RESTClient(),
 	}
 }
 
 // Get takes name of the function, and returns the corresponding function object, and an error if there is any.
-func (c *functions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Function, err error) {
-	result = &v1alpha1.Function{}
+func (c *functions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.Function, err error) {
+	result = &v1beta1.Function{}
 	err = c.client.Get().
 		Resource("functions").
 		Name(name).
@@ -75,12 +75,12 @@ func (c *functions) Get(ctx context.Context, name string, options v1.GetOptions)
 }
 
 // List takes label and field selectors, and returns the list of Functions that match those selectors.
-func (c *functions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.FunctionList, err error) {
+func (c *functions) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.FunctionList, err error) {
 	var timeout time.Duration
 	if opts.TimeoutSeconds != nil {
 		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
 	}
-	result = &v1alpha1.FunctionList{}
+	result = &v1beta1.FunctionList{}
 	err = c.client.Get().
 		Resource("functions").
 		VersionedParams(&opts, scheme.ParameterCodec).
@@ -105,8 +105,8 @@ func (c *functions) Watch(ctx context.Context, opts v1.ListOptions) (watch.Inter
 }
 
 // Create takes the representation of a function and creates it.  Returns the server's representation of the function, and an error, if there is any.
-func (c *functions) Create(ctx context.Context, function *v1alpha1.Function, opts v1.CreateOptions) (result *v1alpha1.Function, err error) {
-	result = &v1alpha1.Function{}
+func (c *functions) Create(ctx context.Context, function *v1beta1.Function, opts v1.CreateOptions) (result *v1beta1.Function, err error) {
+	result = &v1beta1.Function{}
 	err = c.client.Post().
 		Resource("functions").
 		VersionedParams(&opts, scheme.ParameterCodec).
@@ -117,8 +117,8 @@ func (c *functions) Create(ctx context.Context, function *v1alpha1.Function, opt
 }
 
 // Update takes the representation of a function and updates it. Returns the server's representation of the function, and an error, if there is any.
-func (c *functions) Update(ctx context.Context, function *v1alpha1.Function, opts v1.UpdateOptions) (result *v1alpha1.Function, err error) {
-	result = &v1alpha1.Function{}
+func (c *functions) Update(ctx context.Context, function *v1beta1.Function, opts v1.UpdateOptions) (result *v1beta1.Function, err error) {
+	result = &v1beta1.Function{}
 	err = c.client.Put().
 		Resource("functions").
 		Name(function.Name).
@@ -131,8 +131,8 @@ func (c *functions) Update(ctx context.Context, function *v1alpha1.Function, opt
 
 // UpdateStatus was generated because the type contains a Status member.
 // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
-func (c *functions) UpdateStatus(ctx context.Context, function *v1alpha1.Function, opts v1.UpdateOptions) (result *v1alpha1.Function, err error) {
-	result = &v1alpha1.Function{}
+func (c *functions) UpdateStatus(ctx context.Context, function *v1beta1.Function, opts v1.UpdateOptions) (result *v1beta1.Function, err error) {
+	result = &v1beta1.Function{}
 	err = c.client.Put().
 		Resource("functions").
 		Name(function.Name).
@@ -170,8 +170,8 @@ func (c *functions) DeleteCollection(ctx context.Context, opts v1.DeleteOptions,
 }
 
 // Patch applies the patch and returns the patched function.
-func (c *functions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Function, err error) {
-	result = &v1alpha1.Function{}
+func (c *functions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.Function, err error) {
+	result = &v1beta1.Function{}
 	err = c.client.Patch(pt).
 		Resource("functions").
 		Name(name).
diff --git a/internal/client/clientset/versioned/typed/pkg/v1alpha1/functionrevision.go b/internal/client/clientset/versioned/typed/pkg/v1beta1/functionrevision.go
similarity index 78%
rename from internal/client/clientset/versioned/typed/pkg/v1alpha1/functionrevision.go
rename to internal/client/clientset/versioned/typed/pkg/v1beta1/functionrevision.go
index 263ee91a7..f44a3d4b2 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1alpha1/functionrevision.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1beta1/functionrevision.go
@@ -16,13 +16,13 @@ limitations under the License.
 
 // Code generated by client-gen. DO NOT EDIT.
 
-package v1alpha1
+package v1beta1
 
 import (
 	"context"
 	"time"
 
-	v1alpha1 "github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	v1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	scheme "github.com/crossplane/crossplane/internal/client/clientset/versioned/scheme"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	types "k8s.io/apimachinery/pkg/types"
@@ -38,15 +38,15 @@ type FunctionRevisionsGetter interface {
 
 // FunctionRevisionInterface has methods to work with FunctionRevision resources.
 type FunctionRevisionInterface interface {
-	Create(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.CreateOptions) (*v1alpha1.FunctionRevision, error)
-	Update(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.UpdateOptions) (*v1alpha1.FunctionRevision, error)
-	UpdateStatus(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.UpdateOptions) (*v1alpha1.FunctionRevision, error)
+	Create(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.CreateOptions) (*v1beta1.FunctionRevision, error)
+	Update(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.UpdateOptions) (*v1beta1.FunctionRevision, error)
+	UpdateStatus(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.UpdateOptions) (*v1beta1.FunctionRevision, error)
 	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
 	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
-	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.FunctionRevision, error)
-	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.FunctionRevisionList, error)
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta1.FunctionRevision, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1beta1.FunctionRevisionList, error)
 	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
-	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.FunctionRevision, err error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.FunctionRevision, err error)
 	FunctionRevisionExpansion
 }
 
@@ -56,15 +56,15 @@ type functionRevisions struct {
 }
 
 // newFunctionRevisions returns a FunctionRevisions
-func newFunctionRevisions(c *PkgV1alpha1Client) *functionRevisions {
+func newFunctionRevisions(c *PkgV1beta1Client) *functionRevisions {
 	return &functionRevisions{
 		client: c.RESTClient(),
 	}
 }
 
 // Get takes name of the functionRevision, and returns the corresponding functionRevision object, and an error if there is any.
-func (c *functionRevisions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.FunctionRevision, err error) {
-	result = &v1alpha1.FunctionRevision{}
+func (c *functionRevisions) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta1.FunctionRevision, err error) {
+	result = &v1beta1.FunctionRevision{}
 	err = c.client.Get().
 		Resource("functionrevisions").
 		Name(name).
@@ -75,12 +75,12 @@ func (c *functionRevisions) Get(ctx context.Context, name string, options v1.Get
 }
 
 // List takes label and field selectors, and returns the list of FunctionRevisions that match those selectors.
-func (c *functionRevisions) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.FunctionRevisionList, err error) {
+func (c *functionRevisions) List(ctx context.Context, opts v1.ListOptions) (result *v1beta1.FunctionRevisionList, err error) {
 	var timeout time.Duration
 	if opts.TimeoutSeconds != nil {
 		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
 	}
-	result = &v1alpha1.FunctionRevisionList{}
+	result = &v1beta1.FunctionRevisionList{}
 	err = c.client.Get().
 		Resource("functionrevisions").
 		VersionedParams(&opts, scheme.ParameterCodec).
@@ -105,8 +105,8 @@ func (c *functionRevisions) Watch(ctx context.Context, opts v1.ListOptions) (wat
 }
 
 // Create takes the representation of a functionRevision and creates it.  Returns the server's representation of the functionRevision, and an error, if there is any.
-func (c *functionRevisions) Create(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.CreateOptions) (result *v1alpha1.FunctionRevision, err error) {
-	result = &v1alpha1.FunctionRevision{}
+func (c *functionRevisions) Create(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.CreateOptions) (result *v1beta1.FunctionRevision, err error) {
+	result = &v1beta1.FunctionRevision{}
 	err = c.client.Post().
 		Resource("functionrevisions").
 		VersionedParams(&opts, scheme.ParameterCodec).
@@ -117,8 +117,8 @@ func (c *functionRevisions) Create(ctx context.Context, functionRevision *v1alph
 }
 
 // Update takes the representation of a functionRevision and updates it. Returns the server's representation of the functionRevision, and an error, if there is any.
-func (c *functionRevisions) Update(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.UpdateOptions) (result *v1alpha1.FunctionRevision, err error) {
-	result = &v1alpha1.FunctionRevision{}
+func (c *functionRevisions) Update(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.UpdateOptions) (result *v1beta1.FunctionRevision, err error) {
+	result = &v1beta1.FunctionRevision{}
 	err = c.client.Put().
 		Resource("functionrevisions").
 		Name(functionRevision.Name).
@@ -131,8 +131,8 @@ func (c *functionRevisions) Update(ctx context.Context, functionRevision *v1alph
 
 // UpdateStatus was generated because the type contains a Status member.
 // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
-func (c *functionRevisions) UpdateStatus(ctx context.Context, functionRevision *v1alpha1.FunctionRevision, opts v1.UpdateOptions) (result *v1alpha1.FunctionRevision, err error) {
-	result = &v1alpha1.FunctionRevision{}
+func (c *functionRevisions) UpdateStatus(ctx context.Context, functionRevision *v1beta1.FunctionRevision, opts v1.UpdateOptions) (result *v1beta1.FunctionRevision, err error) {
+	result = &v1beta1.FunctionRevision{}
 	err = c.client.Put().
 		Resource("functionrevisions").
 		Name(functionRevision.Name).
@@ -170,8 +170,8 @@ func (c *functionRevisions) DeleteCollection(ctx context.Context, opts v1.Delete
 }
 
 // Patch applies the patch and returns the patched functionRevision.
-func (c *functionRevisions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.FunctionRevision, err error) {
-	result = &v1alpha1.FunctionRevision{}
+func (c *functionRevisions) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta1.FunctionRevision, err error) {
+	result = &v1beta1.FunctionRevision{}
 	err = c.client.Patch(pt).
 		Resource("functionrevisions").
 		Name(name).
diff --git a/internal/client/clientset/versioned/typed/pkg/v1beta1/generated_expansion.go b/internal/client/clientset/versioned/typed/pkg/v1beta1/generated_expansion.go
index eb6e79fd8..d6674748b 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1beta1/generated_expansion.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1beta1/generated_expansion.go
@@ -18,4 +18,8 @@ limitations under the License.
 
 package v1beta1
 
+type FunctionExpansion interface{}
+
+type FunctionRevisionExpansion interface{}
+
 type LockExpansion interface{}
diff --git a/internal/client/clientset/versioned/typed/pkg/v1beta1/pkg_client.go b/internal/client/clientset/versioned/typed/pkg/v1beta1/pkg_client.go
index eae5e4738..3c48d73e2 100644
--- a/internal/client/clientset/versioned/typed/pkg/v1beta1/pkg_client.go
+++ b/internal/client/clientset/versioned/typed/pkg/v1beta1/pkg_client.go
@@ -28,6 +28,8 @@ import (
 
 type PkgV1beta1Interface interface {
 	RESTClient() rest.Interface
+	FunctionsGetter
+	FunctionRevisionsGetter
 	LocksGetter
 }
 
@@ -36,6 +38,14 @@ type PkgV1beta1Client struct {
 	restClient rest.Interface
 }
 
+func (c *PkgV1beta1Client) Functions() FunctionInterface {
+	return newFunctions(c)
+}
+
+func (c *PkgV1beta1Client) FunctionRevisions() FunctionRevisionInterface {
+	return newFunctionRevisions(c)
+}
+
 func (c *PkgV1beta1Client) Locks() LockInterface {
 	return newLocks(c)
 }
diff --git a/internal/controller/apiextensions/composite/composed.go b/internal/controller/apiextensions/composite/composed.go
index 666430422..14b293a5b 100644
--- a/internal/controller/apiextensions/composite/composed.go
+++ b/internal/controller/apiextensions/composite/composed.go
@@ -19,81 +19,36 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
-	iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 )
 
+// A ResourceName uniquely identifies the composed resource within a Composition
+// and within Composition Function gRPC calls. This is not the metadata.name of
+// the actual composed resource instance; rather it is the name of an entry in a
+// Composition's resources array, and/or a RunFunctionRequest's observed/desired
+// resources object.
+type ResourceName string
+
 // A ComposedResource is an output of the composition process.
 type ComposedResource struct {
-	// ResourceName identifies the composed resource within a Composition or
-	// FunctionIO. This is not the metadata.name of the actual composed resource
-	// instance; rather it is the name of an entry in a Composition's resources
-	// array, and/or a FunctionIO's observed/desired resources array.
-	ResourceName string
+	// ResourceName of the composed resource.
+	ResourceName ResourceName
 
 	// Ready indicates whether this composed resource is ready - i.e. whether
 	// all of its readiness checks passed.
 	Ready bool
 }
 
-// ComposedResourceState tracks the state of a composed resource through the
-// Composition process.
+// ComposedResourceState represents a composed resource (either desired or
+// observed).
 type ComposedResourceState struct {
-	// State that is returned to the caller.
-	ComposedResource
-
-	// Things used to produce a composed resource.
-	Template *v1.ComposedTemplate
-	Desired  *iov1alpha1.DesiredResource
-
-	// The state of the composed resource.
-	TemplateRenderErr error
 	Resource          resource.Composed
 	ConnectionDetails managed.ConnectionDetails
+	Ready             bool
 }
 
-// ComposedResourceStates is a map of (Composition) resource name to state. The
-// key corresponds to the ResourceName field of the ComposedResource type.
-type ComposedResourceStates map[string]ComposedResourceState
+// ComposedResourceStates tracks the state of composed resources.
+type ComposedResourceStates map[ResourceName]ComposedResourceState
 
-// Merge the supplied composed resource state into the map of states. See
-// MergeComposedResourceStates for details.
-func (rs ComposedResourceStates) Merge(s ComposedResourceState) {
-	rs[s.ResourceName] = MergeComposedResourceStates(rs[s.ResourceName], s)
-}
-
-// MergeComposedResourceStates merges the new ComposedResourceState into the old
-// one. It is used to update the result of a P&T composition with the result of
-// a subsequent function composition operation on the same composed resource.
-func MergeComposedResourceStates(old, new ComposedResourceState) ComposedResourceState {
-	out := old
-	if new.ResourceName != "" {
-		out.ResourceName = new.ResourceName
-	}
-	if new.Resource != nil {
-		out.Resource = new.Resource
-	}
-	if new.Template != nil {
-		out.Template = new.Template
-	}
-	if new.Desired != nil {
-		out.Desired = new.Desired
-	}
-	// TODO(negz): Should Ready be *bool so we can differentiate between false
-	// and unset? In practice we currently only ever transition _to_ these
-	// states.
-	if new.Ready {
-		out.Ready = new.Ready
-	}
-	if new.TemplateRenderErr != nil {
-		out.TemplateRenderErr = new.TemplateRenderErr
-	}
-	if out.ConnectionDetails == nil {
-		out.ConnectionDetails = make(managed.ConnectionDetails)
-	}
-	for k, v := range new.ConnectionDetails {
-		out.ConnectionDetails[k] = v
-	}
-
-	return out
-}
+// ComposedResourceTemplates are the P&T templates for composed resources.
+type ComposedResourceTemplates map[ResourceName]v1.ComposedTemplate
diff --git a/internal/controller/apiextensions/composite/composite.go b/internal/controller/apiextensions/composite/composite.go
index 3e25a4bab..50eea420f 100644
--- a/internal/controller/apiextensions/composite/composite.go
+++ b/internal/controller/apiextensions/composite/composite.go
@@ -31,14 +31,14 @@ const (
 
 // SetCompositionResourceName sets the name of the composition template used to
 // reconcile a composed resource as an annotation.
-func SetCompositionResourceName(o metav1.Object, name string) {
-	meta.AddAnnotations(o, map[string]string{AnnotationKeyCompositionResourceName: name})
+func SetCompositionResourceName(o metav1.Object, n ResourceName) {
+	meta.AddAnnotations(o, map[string]string{AnnotationKeyCompositionResourceName: string(n)})
 }
 
 // GetCompositionResourceName gets the name of the composition template used to
 // reconcile a composed resource from its annotations.
-func GetCompositionResourceName(o metav1.Object) string {
-	return o.GetAnnotations()[AnnotationKeyCompositionResourceName]
+func GetCompositionResourceName(o metav1.Object) ResourceName {
+	return ResourceName(o.GetAnnotations()[AnnotationKeyCompositionResourceName])
 }
 
 // Returns types of patches that are from a composed resource _to_ a composite resource.
diff --git a/internal/controller/apiextensions/composite/composition_fallback.go b/internal/controller/apiextensions/composite/composition_fallback.go
deleted file mode 100644
index b01df9afe..000000000
--- a/internal/controller/apiextensions/composite/composition_fallback.go
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
-Copyright 2022 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package composite
-
-import (
-	"context"
-
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	"github.com/crossplane/crossplane-runtime/pkg/errors"
-	"github.com/crossplane/crossplane-runtime/pkg/resource"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
-)
-
-// Error strings.
-const (
-	errTriggerFn = "cannot determine which Composer to use"
-)
-
-// A TriggerFn lets a FallBackComposer know when it should fall back from the
-// preferred to the fallback Composer.
-type TriggerFn func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error)
-
-// A FallBackComposer wraps two Composers - one preferred and one fallback. If
-// the trigger FallBackFn returns true it will use the fallback rather than the
-// preferred Composer.
-type FallBackComposer struct {
-	preferred Composer
-	fallback  Composer
-	trigger   TriggerFn
-}
-
-// NewFallBackComposer returns a Composer that calls the preferred Composer
-// unless the supplied TriggerFn triggers a fallback to the fallback Composer.
-func NewFallBackComposer(preferred, fallback Composer, fn TriggerFn) *FallBackComposer {
-	return &FallBackComposer{
-		preferred: preferred,
-		fallback:  fallback,
-		trigger:   fn,
-	}
-}
-
-// Compose calls either the preferred or fallback Composer's Compose method
-// depending on the result of the TriggerFn.
-func (c *FallBackComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) {
-	fallback, err := c.trigger(ctx, xr, req)
-	if err != nil {
-		return CompositionResult{}, errors.Wrap(err, errTriggerFn)
-	}
-	if fallback {
-		return c.fallback.Compose(ctx, xr, req)
-	}
-
-	return c.preferred.Compose(ctx, xr, req)
-}
-
-// FallBackForAnonymousTemplates returns a TriggerFn that triggers a fallback if
-// the supplied Composition uses anonymous templates, or the supplied XR
-// references composed resources that appear to have been created by an
-// anonymous template. We use this to fall back from the PTFComposer to the
-// PTComposer when Composition Functions are enabled.
-//
-// The PTFComposer does not support anonymous templates; it requires named
-// resources to map from P&T resources to FunctionIO resources. A validator
-// ensures that its not possible to create a Composition that uses both
-// Composition Functions and anonymous resource templates.
-func FallBackForAnonymousTemplates(c client.Reader) TriggerFn {
-	return func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
-		// Fall back if any templates are unnamed.
-		for _, t := range req.Revision.Spec.Resources {
-			if t.Name == nil {
-				return true, nil
-			}
-		}
-
-		for _, ref := range xr.GetResourceReferences() {
-			r := composed.New(composed.FromReference(ref))
-			if err := c.Get(ctx, types.NamespacedName{Name: r.GetName()}, r); err != nil {
-				return false, errors.Wrap(resource.IgnoreNotFound(err), errGetComposed)
-			}
-
-			if GetCompositionResourceName(r) == "" {
-				return true, nil
-			}
-		}
-
-		return false, nil
-	}
-}
diff --git a/internal/controller/apiextensions/composite/composition_functions.go b/internal/controller/apiextensions/composite/composition_functions.go
new file mode 100644
index 000000000..df5f42707
--- /dev/null
+++ b/internal/controller/apiextensions/composite/composition_functions.go
@@ -0,0 +1,587 @@
+/*
+Copyright 2022 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+*/
+
+package composite
+
+import (
+	"context"
+	"fmt"
+	"sort"
+
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/types/known/structpb"
+	corev1 "k8s.io/api/core/v1"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/json"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/event"
+	"github.com/crossplane/crossplane-runtime/pkg/meta"
+	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
+	"github.com/crossplane/crossplane-runtime/pkg/resource"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1beta1"
+)
+
+// Error strings.
+const (
+	errFetchXRConnectionDetails = "cannot fetch composite resource connection details"
+	errGetExistingCDs           = "cannot get existing composed resources"
+	errBuildObserved            = "cannot build observed state for RunFunctionRequest"
+	errGarbageCollectCDs        = "cannot garbage collect composed resources that are no longer desired"
+	errApplyXRRefs              = "cannot update composite resource spec.resourceRefs"
+	errApplyXRStatus            = "cannot apply composite resource status"
+	errAnonymousCD              = "encountered composed resource without required \"" + AnnotationKeyCompositionResourceName + "\" annotation"
+	errUnmarshalDesiredXRStatus = "cannot unmarshal desired composite resource status from RunFunctionResponse"
+	errXRAsStruct               = "cannot encode composite resource to protocol buffer Struct well-known type"
+	errStructFromUnstructured   = "cannot create Struct"
+
+	errFmtDryRunCreateCD             = "cannot name (i.e. dry-run create) composed resource %q"
+	errFmtApplyCD                    = "cannot apply composed resource %q"
+	errFmtFetchCDConnectionDetails   = "cannot fetch connection details for composed resource %q (a %s named %s)"
+	errFmtUnmarshalPipelineStepInput = "cannot unmarshal input for Composition pipeline step %q"
+	errFmtRunPipelineStep            = "cannot run Composition pipeline step %q"
+	errFmtDeleteCD                   = "cannot delete composed resource %q (a %s named %s)"
+	errFmtUnmarshalDesiredCD         = "cannot unmarshal desired composed resource %q from RunFunctionResponse"
+	errFmtCDAsStruct                 = "cannot encode composed resource %q to protocol buffer Struct well-known type"
+	errFmtFatalResult                = "pipeline step %q returned a fatal result: %s"
+)
+
+// FunctionFieldOwner is set for fields owned by the FunctionComposer.
+const FunctionFieldOwner = "fn.apiextensions.crossplane.io"
+
+// A FunctionComposer supports composing resources using a pipeline of
+// Composition Functions. It ignores the P&T resources array.
+type FunctionComposer struct {
+	client    client.Client
+	composite xr
+	pipeline  FunctionRunner
+}
+
+type xr struct {
+	managed.ConnectionDetailsFetcher
+	ComposedResourceObserver
+	ComposedResourceGarbageCollector
+}
+
+// A FunctionRunner runs a single Composition Function.
+type FunctionRunner interface {
+	// RunFunction runs the named Composition Function.
+	RunFunction(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (*v1beta1.RunFunctionResponse, error)
+}
+
+// A FunctionRunnerFn is a function that can run a Composition Function.
+type FunctionRunnerFn func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (*v1beta1.RunFunctionResponse, error)
+
+// RunFunction runs the named Composition Function with the supplied request.
+func (fn FunctionRunnerFn) RunFunction(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (*v1beta1.RunFunctionResponse, error) {
+	return fn(ctx, name, req)
+}
+
+// A ComposedResourceObserver observes existing composed resources.
+type ComposedResourceObserver interface {
+	ObserveComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error)
+}
+
+// A ComposedResourceObserverFn observes existing composed resources.
+type ComposedResourceObserverFn func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error)
+
+// ObserveComposedResources observes existing composed resources.
+func (fn ComposedResourceObserverFn) ObserveComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+	return fn(ctx, xr)
+}
+
+// A ComposedResourceGarbageCollector deletes observed composed resources that
+// are no longer desired.
+type ComposedResourceGarbageCollector interface {
+	GarbageCollectComposedResources(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error
+}
+
+// A ComposedResourceGarbageCollectorFn deletes observed composed resources that
+// are no longer desired.
+type ComposedResourceGarbageCollectorFn func(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error
+
+// GarbageCollectComposedResources deletes observed composed resources that are
+// no longer desired.
+func (fn ComposedResourceGarbageCollectorFn) GarbageCollectComposedResources(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error {
+	return fn(ctx, owner, observed, desired)
+}
+
+// A FunctionComposerOption is used to configure a FunctionComposer.
+type FunctionComposerOption func(*FunctionComposer)
+
+// WithCompositeConnectionDetailsFetcher configures how the FunctionComposer should
+// get the composite resource's connection details.
+func WithCompositeConnectionDetailsFetcher(f managed.ConnectionDetailsFetcher) FunctionComposerOption {
+	return func(p *FunctionComposer) {
+		p.composite.ConnectionDetailsFetcher = f
+	}
+}
+
+// WithComposedResourceObserver configures how the FunctionComposer should get existing
+// composed resources.
+func WithComposedResourceObserver(g ComposedResourceObserver) FunctionComposerOption {
+	return func(p *FunctionComposer) {
+		p.composite.ComposedResourceObserver = g
+	}
+}
+
+// WithComposedResourceGarbageCollector configures how the FunctionComposer should
+// garbage collect undesired composed resources.
+func WithComposedResourceGarbageCollector(d ComposedResourceGarbageCollector) FunctionComposerOption {
+	return func(p *FunctionComposer) {
+		p.composite.ComposedResourceGarbageCollector = d
+	}
+}
+
+// NewFunctionComposer returns a new Composer that supports composing resources using
+// both Patch and Transform (P&T) logic and a pipeline of Composition Functions.
+func NewFunctionComposer(kube client.Client, r FunctionRunner, o ...FunctionComposerOption) *FunctionComposer {
+	// TODO(negz): Can we avoid double-wrapping if the supplied client is
+	// already wrapped? Or just do away with unstructured.NewClient completely?
+	kube = unstructured.NewClient(kube)
+
+	f := NewSecretConnectionDetailsFetcher(kube)
+
+	c := &FunctionComposer{
+		client: kube,
+
+		composite: xr{
+			ConnectionDetailsFetcher:         f,
+			ComposedResourceObserver:         NewExistingComposedResourceObserver(kube, f),
+			ComposedResourceGarbageCollector: NewDeletingComposedResourceGarbageCollector(kube),
+		},
+
+		pipeline: r,
+	}
+
+	for _, fn := range o {
+		fn(c)
+	}
+
+	return c
+}
+
+// Compose resources using the Functions pipeline.
+func (c *FunctionComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) { //nolint:gocyclo // We probably don't want any further abstraction for the sake of reduced complexity.
+	// TODO(negz): Change the signature of Compose to take
+	// *composite.Unstructured rather than an interface.
+	uxr, ok := xr.(*composite.Unstructured)
+	if !ok {
+		// This composer uses server-side apply, so we need to make sure we're
+		// working with unstructured data.
+		return CompositionResult{}, errors.Errorf("composite resource must be of type %T", &composite.Unstructured{})
+	}
+
+	// Observe our existing composed resources. We need to do this before we
+	// render any P&T templates, so that we can make sure we use the same
+	// composed resource names (as in, metadata.name) every time. We know what
+	// composed resources exist because we read them from our XR's
+	// spec.resourceRefs, so it's crucial that we never create a composed
+	// resource without first persisting a reference to it.
+	observed, err := c.composite.ObserveComposedResources(ctx, uxr)
+	if err != nil {
+		return CompositionResult{}, errors.Wrap(err, errGetExistingCDs)
+	}
+
+	// Build the initial observed and desired state to be passed to our
+	// Composition Function pipeline. The observed state includes the XR and its
+	// current (persisted) connection details, as well as any existing composed
+	// resource and their current connection details. The desired state includes
+	// only the XR and its connection details, which will initially be identical
+	// to the observed state.
+	xrConns, err := c.composite.FetchConnection(ctx, uxr)
+	if err != nil {
+		return CompositionResult{}, errors.Wrap(err, errFetchXRConnectionDetails)
+	}
+	o, err := AsState(uxr, xrConns, observed)
+	if err != nil {
+		return CompositionResult{}, errors.Wrap(err, errBuildObserved)
+	}
+
+	// The Function pipeline starts with empty desired state.
+	d := &v1beta1.State{}
+
+	events := []event.Event{}
+
+	// Run any Composition Functions in the pipeline. Each Function may mutate
+	// the desired state returned by the last, and each Function may produce
+	// results that will be emitted as events.
+	for _, fn := range req.Revision.Spec.Pipeline {
+		req := &v1beta1.RunFunctionRequest{Observed: o, Desired: d}
+
+		if fn.Input != nil {
+			in := &structpb.Struct{}
+			if err := in.UnmarshalJSON(fn.Input.Raw); err != nil {
+				return CompositionResult{}, errors.Wrapf(err, errFmtUnmarshalPipelineStepInput, fn.Step)
+			}
+			req.Input = in
+		}
+
+		// TODO(negz): Generate a content-addressable tag for this request.
+		// Perhaps using https://github.com/cerbos/protoc-gen-go-hashpb ?
+		rsp, err := c.pipeline.RunFunction(ctx, fn.FunctionRef.Name, req)
+		if err != nil {
+			return CompositionResult{}, errors.Wrapf(err, errFmtRunPipelineStep, fn.Step)
+		}
+
+		d = rsp.GetDesired()
+
+		// Results of fatal severity stop the Composition process. Other results
+		// are accumulated to be emitted as events by the Reconciler.
+		for _, rs := range rsp.Results {
+			switch rs.Severity {
+			case v1beta1.Severity_SEVERITY_FATAL:
+				return CompositionResult{}, errors.Errorf(errFmtFatalResult, fn.Step, rs.Message)
+			case v1beta1.Severity_SEVERITY_WARNING:
+				events = append(events, event.Warning(reasonCompose, errors.Errorf("Pipeline step %q: %s", fn.Step, rs.Message)))
+			case v1beta1.Severity_SEVERITY_NORMAL:
+				events = append(events, event.Normal(reasonCompose, fmt.Sprintf("Pipeline step %q: %s", fn.Step, rs.Message)))
+			case v1beta1.Severity_SEVERITY_UNSPECIFIED:
+				// We could hit this case if a Function was built against a newer
+				// protobuf than this build of Crossplane, and the new protobuf
+				// introduced a severity that we don't know about.
+				events = append(events, event.Warning(reasonCompose, errors.Errorf("Pipeline step %q returned a result of unknown severity (assuming warning): %s", fn.Step, rs.Message)))
+			}
+		}
+	}
+
+	// Load our desired composed resources from the Function pipeline.
+	desired := ComposedResourceStates{}
+	for name, dr := range d.GetResources() {
+		cd := composed.New()
+		if err := FromStruct(cd, dr.GetResource()); err != nil {
+			return CompositionResult{}, errors.Wrapf(err, errFmtUnmarshalDesiredCD, name)
+		}
+
+		// If this desired resource state pertains to an existing composed
+		// resource we want to maintain its name and namespace.
+		or, ok := observed[ResourceName(name)]
+		if ok {
+			cd.SetNamespace(or.Resource.GetNamespace())
+			cd.SetName(or.Resource.GetName())
+		}
+
+		// Set standard composed resource metadata that is derived from the XR.
+		if err := RenderComposedResourceMetadata(cd, uxr, ResourceName(name)); err != nil {
+			return CompositionResult{}, errors.Wrapf(err, errFmtRenderMetadata, name)
+		}
+
+		// We (ab)use dry-run create to generate a unique, available name for
+		// our composed resource using metadata.generateName semantics. We want
+		// to allocate this name before we actually create the resource so that
+		// we can persist a resourceRef to it. This ensures we don't leak
+		// composed resources - see UpdateResourceRefs below.
+		if cd.GetName() == "" {
+			named := cd.DeepCopy()
+			if err := c.client.Create(ctx, named, client.FieldOwner(FunctionFieldOwner), client.DryRunAll); err != nil {
+				return CompositionResult{}, errors.Wrapf(err, errFmtDryRunCreateCD, name)
+			}
+			cd.SetName(named.GetName())
+		}
+
+		// TODO(negz): Should we try to automatically derive readiness if the
+		// Function returns READY_UNSPECIFIED? Is it safe to assume that if the
+		// Function doesn't have an opinion about readiness then we should look
+		// for the Ready: True status condition?
+		desired[ResourceName(name)] = ComposedResourceState{
+			Resource:          cd,
+			ConnectionDetails: dr.GetConnectionDetails(),
+			Ready:             dr.Ready == v1beta1.Ready_READY_TRUE,
+		}
+	}
+
+	// Garbage collect any observed resources that aren't part of our final
+	// desired state. We must do this before we update the XR's resource
+	// references to ensure that we don't forget and leak them if a delete
+	// fails.
+	if err := c.composite.GarbageCollectComposedResources(ctx, uxr, observed, desired); err != nil {
+		return CompositionResult{}, errors.Wrap(err, errGarbageCollectCDs)
+	}
+
+	// Record references to all desired composed resources. We need to do this
+	// before we apply the composed resources in order to avoid potentially
+	// leaking them. For example if we create three composed resources with
+	// randomly generated names and hit an error applying the second one we need
+	// to know that the first one (that _was_ created) exists next time we
+	// reconcile the XR.
+	refs := composite.New()
+	refs.SetAPIVersion(uxr.GetAPIVersion())
+	refs.SetKind(uxr.GetKind())
+	refs.SetName(uxr.GetName())
+	UpdateResourceRefs(refs, desired)
+
+	// Persist our updated composed resource references. We want this to be an
+	// atomic replace of the entire array. Note that we're relying on the status
+	// patch that immediately follows to load the latest version of uxr from the
+	// API server.
+	if err := c.client.Patch(ctx, refs, client.Apply, client.ForceOwnership, client.FieldOwner(FunctionFieldOwner)); err != nil {
+		// It's important we don't proceed if this fails, because we need to be
+		// sure we've persisted our resource references before we create any new
+		// composed resources below.
+		return CompositionResult{}, errors.Wrap(err, errApplyXRRefs)
+	}
+
+	// Our goal here is to patch our XR's status using server-side apply. We
+	// want the resulting, patched object loaded into uxr. We need to pass in
+	// only our "fully specified intent" - i.e. only the fields that we actually
+	// care about. FromStruct will replace uxr's backing map[string]any with the
+	// content of GetResource (i.e. the desired status). We then need to set its
+	// GVK and name so that our client knows what resource to patch.
+	v := uxr.GetAPIVersion()
+	k := uxr.GetKind()
+	n := uxr.GetName()
+	if err := FromStruct(uxr, d.GetComposite().GetResource()); err != nil {
+		return CompositionResult{}, errors.Wrap(err, errUnmarshalDesiredXRStatus)
+	}
+	uxr.SetAPIVersion(v)
+	uxr.SetKind(k)
+	uxr.SetName(n)
+
+	// TODO(negz): Add x-kubernetes-list-type: map to the ConditionedStatus type
+	// from crossplane-runtime and merge on condition type. This would allow a
+	// Function to return a single XR status condition without atomically
+	// replacing the entire list of conditions. We'll still need to figure out a
+	// way to handle lastTransitionTime to avoid it constantly changing.
+
+	if err := c.client.Status().Patch(ctx, uxr, client.Apply, client.ForceOwnership, client.FieldOwner(FunctionFieldOwner)); err != nil {
+		return CompositionResult{}, errors.Wrap(err, errApplyXRStatus)
+	}
+
+	// Produce our array of resources to return to the Reconciler. The
+	// Reconciler uses this array to determine whether the XR is ready.
+	resources := make([]ComposedResource, 0, len(desired))
+
+	// We apply all of our desired resources before we observe them in the loop
+	// below. This ensures that issues observing and processing one composed
+	// resource won't block the application of another.
+	for name, cd := range desired {
+		// We don't need any crossplane-runtime resource.Applicator style apply
+		// options here because server-side apply takes care of everything.
+		// Specifically it will merge rather than replace owner references (e.g.
+		// for Usages), and will fail if we try to add a controller reference to
+		// a resource that already has a different one.
+		if err := c.client.Patch(ctx, cd.Resource, client.Apply, client.ForceOwnership, client.FieldOwner(FunctionFieldOwner)); err != nil {
+			return CompositionResult{}, errors.Wrapf(err, errFmtApplyCD, name)
+		}
+
+		resources = append(resources, ComposedResource{ResourceName: name, Ready: cd.Ready})
+	}
+
+	return CompositionResult{ConnectionDetails: d.GetComposite().GetConnectionDetails(), Composed: resources, Events: events}, nil
+}
+
+// An ExistingComposedResourceObserver uses an XR's resource references to load
+// any existing composed resources from the API server. It also loads their
+// connection details.
+type ExistingComposedResourceObserver struct {
+	resource client.Reader
+	details  managed.ConnectionDetailsFetcher
+}
+
+// NewExistingComposedResourceObserver returns a ComposedResourceGetter that
+// fetches an XR's existing composed resources.
+func NewExistingComposedResourceObserver(c client.Reader, f managed.ConnectionDetailsFetcher) *ExistingComposedResourceObserver {
+	return &ExistingComposedResourceObserver{resource: c, details: f}
+}
+
+// ObserveComposedResources begins building composed resource state by
+// fetching any existing composed resources referenced by the supplied composite
+// resource, as well as their connection details.
+func (g *ExistingComposedResourceObserver) ObserveComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+	ors := ComposedResourceStates{}
+
+	for _, ref := range xr.GetResourceReferences() {
+		// The PTComposer writes references to resources that it didn't actually
+		// render or create. It has to create these placeholder refs because it
+		// supports anonymous (unnamed) resource templates; it needs to be able
+		// associate entries a Composition's spec.resources array with entries
+		// in an XR's spec.resourceRefs array by their index. These references
+		// won't have a name - we won't be able to get them because they don't
+		// reference a resource that actually exists. We make this check to
+		// cover the (hopefully tiny) edge case where an XR has switched from
+		// P&T Composition to Functions, but has one or more composed resources
+		// that have been failing to render.
+		if ref.Name == "" {
+			continue
+		}
+
+		r := composed.New(composed.FromReference(ref))
+		nn := types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}
+		err := g.resource.Get(ctx, nn, r)
+		if kerrors.IsNotFound(err) {
+			// We believe we created this resource, but it doesn't exist.
+			continue
+		}
+		if err != nil {
+			return nil, errors.Wrap(err, errGetComposed)
+		}
+
+		if c := metav1.GetControllerOf(r); c != nil && c.UID != xr.GetUID() {
+			// If we don't control this resource we just pretend it doesn't
+			// exist. We might try to render and re-create it later, but that
+			// should fail because we check the controller ref there too.
+			continue
+		}
+
+		name := GetCompositionResourceName(r)
+		if name == "" {
+			return nil, errors.New(errAnonymousCD)
+		}
+
+		conn, err := g.details.FetchConnection(ctx, r)
+		if err != nil {
+			return nil, errors.Wrapf(err, errFmtFetchCDConnectionDetails, name, r.GetKind(), r.GetName())
+		}
+
+		ors[name] = ComposedResourceState{Resource: r, ConnectionDetails: conn}
+	}
+
+	return ors, nil
+}
+
+// AsState builds state for a RunFunctionRequest from the XR and composed
+// resources.
+func AsState(xr resource.Composite, xc managed.ConnectionDetails, rs ComposedResourceStates) (*v1beta1.State, error) {
+	r, err := AsStruct(xr)
+	if err != nil {
+		return nil, errors.Wrap(err, errXRAsStruct)
+	}
+
+	oxr := &v1beta1.Resource{Resource: r, ConnectionDetails: xc}
+
+	ocds := make(map[string]*v1beta1.Resource)
+	for name, or := range rs {
+		r, err := AsStruct(or.Resource)
+		if err != nil {
+			return nil, errors.Wrapf(err, errFmtCDAsStruct, name)
+		}
+
+		ocds[string(name)] = &v1beta1.Resource{Resource: r, ConnectionDetails: or.ConnectionDetails}
+	}
+
+	return &v1beta1.State{Composite: oxr, Resources: ocds}, nil
+}
+
+// AsStruct converts the supplied object to a protocol buffer Struct well-known
+// type.
+func AsStruct(o runtime.Object) (*structpb.Struct, error) {
+	// If the supplied object is *Unstructured we don't need to round-trip.
+	if u, ok := o.(*kunstructured.Unstructured); ok {
+		s, err := structpb.NewStruct(u.Object)
+		return s, errors.Wrap(err, errStructFromUnstructured)
+	}
+
+	// If the supplied object wraps *Unstructured we don't need to round-trip.
+	if w, ok := o.(unstructured.Wrapper); ok {
+		s, err := structpb.NewStruct(w.GetUnstructured().Object)
+		return s, errors.Wrap(err, errStructFromUnstructured)
+	}
+
+	// Fall back to a JSON round-trip.
+	b, err := json.Marshal(o)
+	if err != nil {
+		return nil, errors.Wrap(err, errMarshalJSON)
+	}
+
+	s := &structpb.Struct{}
+	return s, errors.Wrap(s.UnmarshalJSON(b), errUnmarshalJSON)
+}
+
+// FromStruct populates the supplied object with content loaded from the Struct.
+func FromStruct(o client.Object, s *structpb.Struct) error {
+	// If the supplied object is *Unstructured we don't need to round-trip.
+	if u, ok := o.(*kunstructured.Unstructured); ok {
+		u.Object = s.AsMap()
+		return nil
+	}
+
+	// If the supplied object wraps *Unstructured we don't need to round-trip.
+	if w, ok := o.(unstructured.Wrapper); ok {
+		w.GetUnstructured().Object = s.AsMap()
+		return nil
+	}
+
+	// Fall back to a JSON round-trip.
+	b, err := protojson.Marshal(s)
+	if err != nil {
+		return errors.Wrap(err, errMarshalProtoStruct)
+	}
+
+	return errors.Wrap(json.Unmarshal(b, o), errUnmarshalJSON)
+}
+
+// An DeletingComposedResourceGarbageCollector deletes undesired composed resources from
+// the API server.
+type DeletingComposedResourceGarbageCollector struct {
+	client client.Writer
+}
+
+// NewDeletingComposedResourceGarbageCollector returns a ComposedResourceDeleter that
+// deletes undesired composed resources from the API server.
+func NewDeletingComposedResourceGarbageCollector(c client.Writer) *DeletingComposedResourceGarbageCollector {
+	return &DeletingComposedResourceGarbageCollector{client: c}
+}
+
+// GarbageCollectComposedResources deletes any composed resource that didn't
+// come out the other end of the Composition Function pipeline (i.e. that wasn't
+// in the final desired state after running the pipeline) from the API server.
+func (d *DeletingComposedResourceGarbageCollector) GarbageCollectComposedResources(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error {
+	del := ComposedResourceStates{}
+	for name, cd := range observed {
+		if _, ok := desired[name]; !ok {
+			del[name] = cd
+		}
+	}
+
+	for name, cd := range del {
+		// We want to garbage collect this resource, but we don't control it.
+		if c := metav1.GetControllerOf(cd.Resource); c == nil || c.UID != owner.GetUID() {
+			continue
+		}
+
+		if err := d.client.Delete(ctx, cd.Resource); resource.IgnoreNotFound(err) != nil {
+			return errors.Wrapf(err, errFmtDeleteCD, name, cd.Resource.GetObjectKind().GroupVersionKind().Kind, cd.Resource.GetName())
+		}
+	}
+
+	return nil
+}
+
+// UpdateResourceRefs updates the supplied state to ensure the XR references all
+// composed resources that exist or are pending creation.
+func UpdateResourceRefs(xr resource.ComposedResourcesReferencer, desired ComposedResourceStates) {
+	refs := make([]corev1.ObjectReference, 0, len(desired))
+	for _, dr := range desired {
+		ref := meta.ReferenceTo(dr.Resource, dr.Resource.GetObjectKind().GroupVersionKind())
+		refs = append(refs, *ref)
+	}
+
+	// We want to ensure our refs are stable.
+	sort.Slice(refs, func(i, j int) bool {
+		ri, rj := refs[i], refs[j]
+		return ri.APIVersion+ri.Kind+ri.Name < rj.APIVersion+rj.Kind+rj.Name
+	})
+
+	xr.SetResourceReferences(refs)
+}
diff --git a/internal/controller/apiextensions/composite/composition_pt.go b/internal/controller/apiextensions/composite/composition_pt.go
index bd16d04c2..adea32868 100644
--- a/internal/controller/apiextensions/composite/composition_pt.go
+++ b/internal/controller/apiextensions/composite/composition_pt.go
@@ -24,7 +24,6 @@ import (
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/json"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -38,28 +37,25 @@ import (
 
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 	"github.com/crossplane/crossplane/internal/controller/apiextensions/usage"
-	"github.com/crossplane/crossplane/internal/xcrd"
 )
 
 // Error strings
 const (
-	errGetComposed      = "cannot get composed resource"
-	errGCComposed       = "cannot garbage collect composed resource"
-	errApply            = "cannot apply composed resource"
-	errFetchDetails     = "cannot fetch connection details"
-	errExtractDetails   = "cannot extract composite resource connection details from composed resource"
-	errReadiness        = "cannot check whether composed resource is ready"
-	errUnmarshal        = "cannot unmarshal base template"
-	errGetSecret        = "cannot get connection secret of composed resource"
-	errNamePrefix       = "name prefix is not found in labels"
-	errKindChanged      = "cannot change the kind of an existing composed resource"
-	errName             = "cannot use dry-run create to name composed resource"
-	errInline           = "cannot inline Composition patch sets"
-	errRenderCR         = "cannot render composite resource"
-	errSetControllerRef = "cannot set controller reference"
-
-	errFmtResourceName = "composed resource %q"
-	errFmtPatch        = "cannot apply the patch at index %d"
+	errGetComposed   = "cannot get composed resource"
+	errGCComposed    = "cannot garbage collect composed resource"
+	errApplyComposed = "cannot apply composed resource"
+	errFetchDetails  = "cannot fetch connection details"
+	errInline        = "cannot inline Composition patch sets"
+
+	errFmtPatchEnvironment             = "cannot apply environment patch at index %d"
+	errFmtParseBase                    = "cannot parse base template of composed resource %q"
+	errFmtRenderFromCompositePatches   = "cannot render FromComposite patches for composed resource %q"
+	errFmtRenderToCompositePatches     = "cannot render ToComposite patches for composed resource %q"
+	errFmtRenderFromEnvironmentPatches = "cannot render FromEnvironment patches for composed resource %q"
+	errFmtRenderMetadata               = "cannot render metadata for composed resource %q"
+	errFmtDryRunApply                  = "cannot dry-run apply composed resource %q"
+	errFmtExtractDetails               = "cannot extract composite resource connection details from composed resource %q"
+	errFmtCheckReadiness               = "cannot check whether composed resource %q is ready"
 )
 
 // TODO(negz): Move P&T Composition logic into its own package?
@@ -75,19 +71,12 @@ func WithTemplateAssociator(a CompositionTemplateAssociator) PTComposerOption {
 	}
 }
 
-// WithCompositeRenderer configures how a PatchAndTransformComposer renders the
-// composite resource.
-func WithCompositeRenderer(r Renderer) PTComposerOption {
+// WithComposedDryRunRenderer configures how the PTComposer should dry-run
+// render composed resources - i.e. by submitting them to the API server to
+// generate a name for them.
+func WithComposedDryRunRenderer(r DryRunRenderer) PTComposerOption {
 	return func(c *PTComposer) {
-		c.composite = r
-	}
-}
-
-// WithComposedRenderer configures how a PatchAndTransformComposer renders
-// composed resources.
-func WithComposedRenderer(r Renderer) PTComposerOption {
-	return func(c *PTComposer) {
-		c.composed.Renderer = r
+		c.composed.DryRunRenderer = r
 	}
 }
 
@@ -117,7 +106,7 @@ func WithComposedConnectionDetailsExtractor(e ConnectionDetailsExtractor) PTComp
 }
 
 type composedResource struct {
-	Renderer
+	DryRunRenderer
 	managed.ConnectionDetailsFetcher
 	ConnectionDetailsExtractor
 	ReadinessChecker
@@ -130,7 +119,6 @@ type composedResource struct {
 type PTComposer struct {
 	client resource.ClientApplicator
 
-	composite   Renderer
 	composition CompositionTemplateAssociator
 	composed    composedResource
 }
@@ -145,15 +133,9 @@ func NewPTComposer(kube client.Client, o ...PTComposerOption) *PTComposer {
 	c := &PTComposer{
 		client: resource.ClientApplicator{Client: kube, Applicator: resource.NewAPIPatchingApplicator(kube)},
 
-		// TODO(negz): Once Composition Functions are GA this Composer will only
-		// need to handle legacy Compositions that use anonymous templates. This
-		// means we will be able to delete the GarbageCollectingAssociator and
-		// just use AssociateByOrder. Compositions with named templates will be
-		// handled by the PTFComposer.
-		composite:   RendererFn(RenderComposite),
 		composition: NewGarbageCollectingAssociator(kube),
 		composed: composedResource{
-			Renderer:                   NewAPIDryRunRenderer(kube),
+			DryRunRenderer:             NewAPIDryRunRenderer(kube),
 			ReadinessChecker:           ReadinessCheckerFn(IsReady),
 			ConnectionDetailsFetcher:   NewSecretConnectionDetailsFetcher(kube),
 			ConnectionDetailsExtractor: ConnectionDetailsExtractorFn(ExtractConnectionDetails),
@@ -168,7 +150,15 @@ func NewPTComposer(kube client.Client, o ...PTComposerOption) *PTComposer {
 }
 
 // Compose resources using the bases, patches, and transforms specified by the
-// supplied Composition.
+// supplied Composition. This reconciler supports only Patch & Transform
+// Composition (not the Function pipeline). It does this in roughly four steps:
+//
+//  1. Figure out which templates are associated with which existing composed
+//     resources, if any.
+//  2. Render from those templates into new or existing composed resources.
+//  3. Apply all composed resources that rendered successfully.
+//  4. Observe the readiness and connection details of all composed resources
+//     that rendered successfully.
 func (c *PTComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) { //nolint:gocyclo // Breaking this up doesn't seem worth yet more layers of abstraction.
 	// Inline PatchSets before composing resources.
 	ct, err := ComposedTemplates(req.Revision.Spec.PatchSets, req.Revision.Spec.Resources)
@@ -176,6 +166,13 @@ func (c *PTComposer) Compose(ctx context.Context, xr resource.Composite, req Com
 		return CompositionResult{}, errors.Wrap(err, errInline)
 	}
 
+	// Figure out which templates are associated with which existing composed
+	// resources. This results in an array of templates associated with an array
+	// of entries in the XR's spec.resourceRefs array. If we're using a
+	// Composition with anonymous resource templates they'll be associated
+	// strictly by order. If we're using a Composition with named resource
+	// templates we'll be able to instead read the template name annotation from
+	// the composed resources to make the annotation.
 	tas, err := c.composition.AssociateTemplates(ctx, xr, ct)
 	if err != nil {
 		return CompositionResult{}, errors.Wrap(err, errAssociate)
@@ -199,7 +196,7 @@ func (c *PTComposer) Compose(ctx context.Context, xr resource.Composite, req Com
 	// input. Errors are recorded, but not considered fatal to the composition
 	// process.
 	refs := make([]corev1.ObjectReference, len(tas))
-	cds := make([]ComposedResourceState, len(tas))
+	cds := make([]resource.Composed, len(tas))
 	for i := range tas {
 		ta := tas[i]
 
@@ -207,18 +204,51 @@ func (c *PTComposer) Compose(ctx context.Context, xr resource.Composite, req Com
 		name := pointer.StringDeref(ta.Template.Name, fmt.Sprintf("resource %d", i+1))
 		r := composed.New(composed.FromReference(ta.Reference))
 
-		rerr := c.composed.Render(ctx, xr, r, ta.Template, req.Environment)
-		if rerr != nil {
-			events = append(events, event.Warning(reasonCompose, errors.Wrapf(rerr, errFmtResourceName, name)))
+		if err := RenderFromJSON(r, ta.Template.Base.Raw); err != nil {
+			// We consider this a terminal error, since it indicates a broken
+			// CompositionRevision that will never be valid.
+			return CompositionResult{}, errors.Wrapf(err, errFmtParseBase, name)
 		}
 
-		cds[i] = ComposedResourceState{
-			ComposedResource:  ComposedResource{ResourceName: name},
-			TemplateRenderErr: rerr,
-			Template:          &ta.Template,
-			Resource:          r,
+		// Failures to patch aren't terminal - we just emit a warning event and
+		// move on. This is because patches often fail because other patches
+		// need to happen first in order for them to succeed. If we returned an
+		// error when a patch failed we might never reach the patch that would
+		// unblock it.
+
+		rendered := true
+		if err := RenderFromCompositePatches(r, xr, ta.Template.Patches); err != nil {
+			events = append(events, event.Warning(reasonCompose, errors.Wrapf(err, errFmtRenderFromCompositePatches, name)))
+			rendered = false
+		}
+
+		if err = RenderToAndFromEnvironmentPatches(r, req.Environment, ta.Template.Patches); err != nil {
+			events = append(events, event.Warning(reasonCompose, errors.Wrapf(err, errFmtRenderFromEnvironmentPatches, name)))
+			rendered = false
 		}
+
+		if err := RenderComposedResourceMetadata(r, xr, ResourceName(pointer.StringDeref(ta.Template.Name, ""))); err != nil {
+			events = append(events, event.Warning(reasonCompose, errors.Wrapf(err, errFmtRenderMetadata, name)))
+			rendered = false
+		}
+
+		if err := c.composed.DryRunRender(ctx, r); err != nil {
+			events = append(events, event.Warning(reasonCompose, errors.Wrapf(err, errFmtDryRunApply, name)))
+			rendered = false
+		}
+
+		// We record a reference even if we didn't render the resource because
+		// if it already exists we don't want to drop our reference to it (and
+		// thus not know about it next reconcile). If we're using anonymous
+		// resource templates we also need to record a reference even if it's
+		// empty, so that our XR's spec.resourceRefs remains the same length and
+		// order as our CompositionRevisions's array of templates.
 		refs[i] = *meta.ReferenceTo(r, r.GetObjectKind().GroupVersionKind())
+
+		// We only need the composed resource if it rendered correctly.
+		if rendered {
+			cds[i] = r
+		}
 	}
 
 	// We persist references to our composed resources before we create
@@ -230,52 +260,80 @@ func (c *PTComposer) Compose(ctx context.Context, xr resource.Composite, req Com
 		return CompositionResult{}, errors.Wrap(err, errUpdate)
 	}
 
-	// We apply all of our composed resources before we observe them and update
-	// in the loop below. This ensures that issues observing and processing one
+	// We apply all of our composed resources before we observe them in the
+	// loop below. This ensures that issues observing and processing one
 	// composed resource won't block the application of another.
-	for _, cd := range cds {
+	for i := range tas {
+		t := tas[i].Template
+		cd := cds[i]
+
 		// If we were unable to render the composed resource we should not try
-		// and apply it.
-		if cd.TemplateRenderErr != nil {
+		// and apply it. The risk of doing so is that we successfully apply a
+		// partially-rendered composed resource that we can't later fix (e.g.
+		// due to an immutable field).
+		if cd == nil {
 			continue
 		}
+
 		o := []resource.ApplyOption{resource.MustBeControllableBy(xr.GetUID()), usage.RespectOwnerRefs()}
-		o = append(o, mergeOptions(filterPatches(cd.Template.Patches, patchTypesFromXR()...))...)
-		if err := c.client.Apply(ctx, cd.Resource, o...); err != nil {
-			return CompositionResult{}, errors.Wrap(err, errApply)
+		o = append(o, mergeOptions(filterPatches(t.Patches, patchTypesFromXR()...))...)
+		if err := c.client.Apply(ctx, cd, o...); err != nil {
+			// TODO(negz): Include the template name (if any) in this error.
+			// Including the rendered resource's kind may help too (e.g. if the
+			// template is anonymous).
+			return CompositionResult{}, errors.Wrap(err, errApplyComposed)
 		}
 	}
 
-	conn := managed.ConnectionDetails{}
-	for i := range cds {
+	// Produce our array of resources to return to the Reconciler. The
+	// Reconciler uses this array to determine whether the XR is ready. This
+	// means it's important that we return a resources resource for every entry
+	// in tas - i.e. a resources resource for every resource template.
+	resources := make([]ComposedResource, len(tas))
+	xrConnDetails := managed.ConnectionDetails{}
+	for i := range tas {
+		t := tas[i].Template
+		cd := cds[i]
+
+		// If this resource is anonymous its "name" is just its index within the
+		// array of composed resource templates.
+		name := ResourceName(pointer.StringDeref(t.Name, fmt.Sprintf("resource %d", i+1)))
+
 		// If we were unable to render the composed resource we should not try
-		// to observe it.
-		if cds[i].TemplateRenderErr != nil {
+		// to observe it. We still want to return it to the Reconciler so that
+		// it knows that this desired composed resource is not ready.
+		if cd == nil {
+			resources[i] = ComposedResource{ResourceName: name, Ready: false}
 			continue
 		}
 
-		if err := c.composite.Render(ctx, xr, cds[i].Resource, *cds[i].Template, req.Environment); err != nil {
-			return CompositionResult{}, errors.Wrap(err, errRenderCR)
+		if err := RenderToCompositePatches(xr, cd, t.Patches); err != nil {
+			// Failures to render ToComposite patches are terminal because this
+			// indicates a Required ToCompositeFieldPath patch failed; i.e. the
+			// composite was _required_ to be patched, but wasn't.
+			return CompositionResult{}, errors.Wrapf(err, errFmtRenderToCompositePatches, name)
 		}
 
-		cds[i].ConnectionDetails, err = c.composed.FetchConnection(ctx, cds[i].Resource)
+		cdConnDetails, err := c.composed.FetchConnection(ctx, cd)
 		if err != nil {
 			return CompositionResult{}, errors.Wrap(err, errFetchDetails)
 		}
 
-		e, err := c.composed.ExtractConnection(cds[i].Resource, cds[i].ConnectionDetails, ExtractConfigsFromTemplate(cds[i].Template)...)
+		extracted, err := c.composed.ExtractConnection(cd, cdConnDetails, ExtractConfigsFromComposedTemplate(&t)...)
 		if err != nil {
-			return CompositionResult{}, errors.Wrap(err, errExtractDetails)
+			return CompositionResult{}, errors.Wrapf(err, errFmtExtractDetails, name)
 		}
 
-		for key, val := range e {
-			conn[key] = val
+		for key, val := range extracted {
+			xrConnDetails[key] = val
 		}
 
-		cds[i].Ready, err = c.composed.IsReady(ctx, cds[i].Resource, ReadinessChecksFromComposedTemplate(cds[i].Template)...)
+		ready, err := c.composed.IsReady(ctx, cd, ReadinessChecksFromComposedTemplate(&t)...)
 		if err != nil {
-			return CompositionResult{}, errors.Wrap(err, errReadiness)
+			return CompositionResult{}, errors.Wrapf(err, errFmtCheckReadiness, name)
 		}
+
+		resources[i] = ComposedResource{ResourceName: name, Ready: ready}
 	}
 
 	// Call Apply so that we do not just replace fields on existing XR but
@@ -296,12 +354,7 @@ func (c *PTComposer) Compose(ctx context.Context, xr resource.Composite, req Com
 		return CompositionResult{}, errors.Wrap(err, errUpdate)
 	}
 
-	out := make([]ComposedResource, len(cds))
-	for i := range cds {
-		out[i] = cds[i].ComposedResource
-	}
-
-	return CompositionResult{ConnectionDetails: conn, Composed: out, Events: events}, nil
+	return CompositionResult{ConnectionDetails: xrConnDetails, Composed: resources, Events: events}, nil
 }
 
 // toXRPatchesFromTAs selects patches defined in composed templates,
@@ -392,7 +445,7 @@ func NewGarbageCollectingAssociator(c client.Client) *GarbageCollectingAssociato
 
 // AssociateTemplates with composed resources.
 func (a *GarbageCollectingAssociator) AssociateTemplates(ctx context.Context, cr resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) { //nolint:gocyclo // Only slightly over (13).
-	templates := map[string]int{}
+	templates := map[ResourceName]int{}
 	for i, t := range ct {
 		if t.Name == nil {
 			// If our templates aren't named we fall back to assuming that the
@@ -400,7 +453,7 @@ func (a *GarbageCollectingAssociator) AssociateTemplates(ctx context.Context, cr
 			// order of our resource template array.
 			return AssociateByOrder(ct, cr.GetResourceReferences()), nil
 		}
-		templates[*t.Name] = i
+		templates[ResourceName(*t.Name)] = i
 	}
 
 	tas := make([]TemplateAssociation, len(ct))
@@ -446,11 +499,8 @@ func (a *GarbageCollectingAssociator) AssociateTemplates(ctx context.Context, cr
 			continue
 		}
 
-		// TODO(negz): Below should be || not &&. If the controller ref is nil
-		// we don't control the resource and shouldn't delete it.
-
 		// We want to garbage collect this resource, but we don't control it.
-		if c := metav1.GetControllerOf(cd); c != nil && c.UID != cr.GetUID() {
+		if c := metav1.GetControllerOf(cd); c == nil || c.UID != cr.GetUID() {
 			continue
 		}
 
@@ -470,114 +520,3 @@ type Observation struct {
 	ConnectionDetails managed.ConnectionDetails
 	Ready             bool
 }
-
-// A RenderFn renders the supplied composed resource.
-type RenderFn func(cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate) error
-
-// Render calls RenderFn.
-func (c RenderFn) Render(cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate) error {
-	return c(cp, cd, t)
-}
-
-// An APIDryRunRenderer renders composed resources. It may perform a dry-run
-// create against an API server in order to name and validate the rendered
-// resource.
-type APIDryRunRenderer struct {
-	client client.Client
-}
-
-// NewAPIDryRunRenderer returns a Renderer of composed resources that may
-// perform a dry-run create against an API server in order to name and validate
-// it.
-func NewAPIDryRunRenderer(c client.Client) *APIDryRunRenderer {
-	return &APIDryRunRenderer{client: c}
-}
-
-// Render the supplied composed resource using the supplied composite resource
-// and template. The rendered resource may be submitted to an API server via a
-// dry run create in order to name and validate it.
-func (r *APIDryRunRenderer) Render(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error { //nolint:gocyclo // Only slightly over (11).
-	kind := cd.GetObjectKind().GroupVersionKind().Kind
-	name := cd.GetName()
-	namespace := cd.GetNamespace()
-
-	if err := json.Unmarshal(t.Base.Raw, cd); err != nil {
-		return errors.Wrap(err, errUnmarshal)
-	}
-
-	// We think this composed resource exists, but when we rendered its template
-	// its kind changed. This shouldn't happen. Either someone changed the kind
-	// in the template or we're trying to use the wrong template (e.g. because
-	// the order of an array of anonymous templates changed).
-	if kind != "" && cd.GetObjectKind().GroupVersionKind().Kind != kind {
-		return errors.New(errKindChanged)
-	}
-
-	if cp.GetLabels()[xcrd.LabelKeyNamePrefixForComposed] == "" {
-		return errors.New(errNamePrefix)
-	}
-
-	// Unmarshalling the template will overwrite any existing fields, so we must
-	// restore the existing name, if any. We also set generate name in case we
-	// haven't yet named this composed resource.
-	cd.SetGenerateName(cp.GetLabels()[xcrd.LabelKeyNamePrefixForComposed] + "-")
-	cd.SetName(name)
-	cd.SetNamespace(namespace)
-
-	for i := range t.Patches {
-		if err := Apply(t.Patches[i], cp, cd, patchTypesFromXR()...); err != nil {
-			return errors.Wrapf(err, errFmtPatch, i)
-		}
-		if env != nil {
-			if err := ApplyToObjects(t.Patches[i], env, cd, patchTypesFromToEnvironment()...); err != nil {
-				return errors.Wrapf(err, errFmtPatch, i)
-			}
-		}
-	}
-
-	// Composed labels and annotations should be rendered after patches are applied
-	meta.AddLabels(cd, map[string]string{
-		xcrd.LabelKeyNamePrefixForComposed: cp.GetLabels()[xcrd.LabelKeyNamePrefixForComposed],
-		xcrd.LabelKeyClaimName:             cp.GetLabels()[xcrd.LabelKeyClaimName],
-		xcrd.LabelKeyClaimNamespace:        cp.GetLabels()[xcrd.LabelKeyClaimNamespace],
-	})
-
-	if t.Name != nil {
-		SetCompositionResourceName(cd, *t.Name)
-	}
-
-	// We do this last to ensure that a Composition cannot influence controller references.
-	or := meta.AsController(meta.TypedReferenceTo(cp, cp.GetObjectKind().GroupVersionKind()))
-	if err := meta.AddControllerReference(cd, or); err != nil {
-		return errors.Wrap(err, errSetControllerRef)
-	}
-
-	// We don't want to dry-run create a resource that can't be named by the API
-	// server due to a missing generate name. We also don't want to create one
-	// that is already named, because doing so will result in an error. The API
-	// server seems to respond with a 500 ServerTimeout error for all dry-run
-	// failures, so we can't just perform a dry-run and ignore 409 Conflicts for
-	// resources that are already named.
-	if cd.GetName() != "" || cd.GetGenerateName() == "" {
-		return nil
-	}
-
-	// The API server returns an available name derived from generateName when
-	// we perform a dry-run create. This name is likely (but not guaranteed) to
-	// be available when we create the composed resource. If the API server
-	// generates a name that is unavailable it will return a 500 ServerTimeout
-	// error.
-	return errors.Wrap(r.client.Create(ctx, cd, client.DryRunAll), errName)
-}
-
-// RenderComposite renders the supplied composite resource using the supplied composed
-// resource and template.
-func RenderComposite(_ context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, _ *Environment) error {
-	for i, p := range t.Patches {
-		if err := Apply(p, cp, cd, patchTypesToXR()...); err != nil {
-			return errors.Wrapf(err, errFmtPatch, i)
-		}
-	}
-
-	return nil
-}
diff --git a/internal/controller/apiextensions/composite/composition_ptf.go b/internal/controller/apiextensions/composite/composition_ptf.go
deleted file mode 100644
index 36b53f64b..000000000
--- a/internal/controller/apiextensions/composite/composition_ptf.go
+++ /dev/null
@@ -1,1083 +0,0 @@
-/*
-Copyright 2022 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License"); you may not use
-this file except in compliance with the License. You may obtain a copy of the
-License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software distributed
-under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
-CONDITIONS OF ANY KIND, either express or implied. See the License for the
-specific language governing permissions and limitations under the License.
-*/
-
-package composite
-
-import (
-	"context"
-	"sort"
-
-	"github.com/google/go-containerregistry/pkg/authn/k8schain"
-	"github.com/google/go-containerregistry/pkg/name"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/credentials/insecure"
-	"google.golang.org/protobuf/types/known/durationpb"
-	corev1 "k8s.io/api/core/v1"
-	kerrors "k8s.io/apimachinery/pkg/api/errors"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/json"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/yaml"
-
-	"github.com/crossplane/crossplane-runtime/pkg/errors"
-	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/meta"
-	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
-	"github.com/crossplane/crossplane-runtime/pkg/resource"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
-
-	iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
-	fnv1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1alpha1"
-	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
-	"github.com/crossplane/crossplane/internal/controller/apiextensions/usage"
-	"github.com/crossplane/crossplane/internal/xcrd"
-)
-
-// Error strings.
-const (
-	errFetchXRConnectionDetails = "cannot fetch composite resource connection details"
-	errGetExistingCDs           = "cannot get existing composed resources"
-	errBuildFunctionIOObserved  = "cannot build FunctionIO observed state"
-	errBuildFunctionIODesired   = "cannot build initial FunctionIO desired state"
-	errMarshalXR                = "cannot marshal composite resource"
-	errMarshalCD                = "cannot marshal composed resource"
-	errParseImage               = "cannot parse image reference"
-	errGetServiceAccount        = "cannot get image pull secrets from service account"
-	errGetImagePullSecret       = "cannot get image pull secret"
-	errNewKeychain              = "cannot create a new in-cluster registry authentication keychain"
-	errResolveKeychain          = "cannot resolve in-cluster registry authentication keychain"
-	errAuthCfg                  = "cannot get in-cluster registry authentication credentials"
-	errPatchAndTransform        = "cannot patch and transform"
-	errRunFunctionPipeline      = "cannot run Composition Function pipeline"
-	errDeleteUndesiredCDs       = "cannot delete undesired composed resources"
-	errApplyXR                  = "cannot apply composite resource"
-	errObserveCDs               = "cannot observe composed resources"
-	errAnonymousCD              = "encountered composed resource without required \"" + AnnotationKeyCompositionResourceName + "\" annotation"
-	errUnmarshalDesiredXR       = "cannot unmarshal desired composite resource from FunctionIO"
-	errUnmarshalDesiredCD       = "cannot unmarshal desired composed resource from FunctionIO"
-	errMarshalFnIO              = "cannot marshal input FunctionIO"
-	errDialRunner               = "cannot dial container runner"
-	errApplyRunFunctionOption   = "cannot apply run function option"
-	errRunFnContainer           = "cannot run container"
-	errCloseRunner              = "cannot close connection to container runner"
-	errUnmarshalFnIO            = "cannot unmarshal output FunctionIO"
-	errFatalResult              = "fatal function pipeline result"
-
-	errFmtApplyCD                  = "cannot apply composed resource %q"
-	errFmtFetchCDConnectionDetails = "cannot fetch connection details for composed resource %q (a %s named %s)"
-	errFmtRenderXR                 = "cannot render composite resource from composed resource %q (a %s named %s)"
-	errFmtRunFn                    = "cannot run function %q"
-
-	errFmtUnsupportedFnType        = "unsupported function type %q"
-	errFmtParseDesiredCD           = "cannot parse desired composed resource %q from FunctionIO"
-	errFmtDeleteCD                 = "cannot delete composed resource %q (a %s named %s)"
-	errFmtReadiness                = "cannot determine whether composed resource %q (a %s named %s) is ready"
-	errFmtExtractConnectionDetails = "cannot extract connection details from composed resource %q (a %s named %s)"
-)
-
-// DefaultTarget is the default function runner target endpoint.
-const DefaultTarget = "unix-abstract:crossplane/fn/default.sock"
-
-// A PTFComposer (i.e. Patch, Transform, and Function Composer) supports
-// composing resources using both Patch and Transform (P&T) logic and a pipeline
-// of Composition Functions. Callers may mix P&T with Composition Functions or
-// use only one or the other. It does not support anonymous, unnamed resource
-// templates and will panic if it encounters one.
-type PTFComposer struct {
-	client resource.ClientApplicator
-
-	composite   ptfComposite
-	composition ptfComposition
-}
-
-type ptfComposite struct {
-	managed.ConnectionDetailsFetcher
-	ComposedResourceGetter
-	ComposedResourceDeleter
-	ComposedResourceObserver
-}
-
-type ptfComposition struct {
-	PatchAndTransformer
-	FunctionPipelineRunner
-}
-
-// A ComposedResourceGetter gets composed resource state.
-type ComposedResourceGetter interface {
-	GetComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error)
-}
-
-// A ComposedResourceGetterFn gets composed resource state.
-type ComposedResourceGetterFn func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error)
-
-// GetComposedResources gets composed resource state.
-func (fn ComposedResourceGetterFn) GetComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-	return fn(ctx, xr)
-}
-
-// A ComposedResourceDeleter deletes composed resources (and their state).
-type ComposedResourceDeleter interface {
-	DeleteComposedResources(ctx context.Context, s *PTFCompositionState) error
-}
-
-// A ComposedResourceDeleterFn deletes composed resources (and their state).
-type ComposedResourceDeleterFn func(ctx context.Context, s *PTFCompositionState) error
-
-// DeleteComposedResources deletes composed resources (and their state).
-func (fn ComposedResourceDeleterFn) DeleteComposedResources(ctx context.Context, s *PTFCompositionState) error {
-	return fn(ctx, s)
-}
-
-// A ComposedResourceObserver derives additional state by observing composed
-// resources.
-type ComposedResourceObserver interface {
-	ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error
-}
-
-// An ComposedResourceObserverFn derives additional state by observing composed
-// resources.
-type ComposedResourceObserverFn func(ctx context.Context, s *PTFCompositionState) error
-
-// ObserveComposedResources derives additional state by observing composed
-// resources.
-func (fn ComposedResourceObserverFn) ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error {
-	return fn(ctx, s)
-}
-
-// A PatchAndTransformer runs P&T Composition.
-type PatchAndTransformer interface {
-	PatchAndTransform(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error
-}
-
-// A PatchAndTransformerFn runs P&T Composition.
-type PatchAndTransformerFn func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error
-
-// PatchAndTransform runs P&T Composition.
-func (fn PatchAndTransformerFn) PatchAndTransform(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-	return fn(ctx, req, s)
-}
-
-// A FunctionPipelineRunner runs a pipeline of Composition Functions.
-type FunctionPipelineRunner interface {
-	RunFunctionPipeline(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error
-}
-
-// A FunctionPipelineRunnerFn runs a pipeline of Composition Functions.
-type FunctionPipelineRunnerFn func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error
-
-// RunFunctionPipeline runs a pipeline of Composition Functions.
-func (fn FunctionPipelineRunnerFn) RunFunctionPipeline(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-	return fn(ctx, req, s, o, d)
-}
-
-// A PTFComposerOption is used to configure a PTFComposer.
-type PTFComposerOption func(*PTFComposer)
-
-// WithCompositeConnectionDetailsFetcher configures how the PTFComposer should
-// get the composite resource's connection details.
-func WithCompositeConnectionDetailsFetcher(f managed.ConnectionDetailsFetcher) PTFComposerOption {
-	return func(p *PTFComposer) {
-		p.composite.ConnectionDetailsFetcher = f
-	}
-}
-
-// WithComposedResourceGetter configures how the PTFComposer should get existing
-// composed resources.
-func WithComposedResourceGetter(g ComposedResourceGetter) PTFComposerOption {
-	return func(p *PTFComposer) {
-		p.composite.ComposedResourceGetter = g
-	}
-}
-
-// WithComposedResourceDeleter configures how the PTFComposer should delete
-// undesired composed resources.
-func WithComposedResourceDeleter(d ComposedResourceDeleter) PTFComposerOption {
-	return func(p *PTFComposer) {
-		p.composite.ComposedResourceDeleter = d
-	}
-}
-
-// WithComposedResourceObserver configures how the PTFComposer should observe
-// composed resources after applying them.
-func WithComposedResourceObserver(o ComposedResourceObserver) PTFComposerOption {
-	return func(p *PTFComposer) {
-		p.composite.ComposedResourceObserver = o
-	}
-}
-
-// WithPatchAndTransformer configures how the PTFComposer should run Patch &
-// Transform (P&T) Composition.
-func WithPatchAndTransformer(pt PatchAndTransformer) PTFComposerOption {
-	return func(p *PTFComposer) {
-		p.composition.PatchAndTransformer = pt
-	}
-}
-
-// WithFunctionPipelineRunner configures how the PTFComposer should run a
-// pipeline of Composition Functions.
-func WithFunctionPipelineRunner(r FunctionPipelineRunner) PTFComposerOption {
-	return func(p *PTFComposer) {
-		p.composition.FunctionPipelineRunner = r
-	}
-}
-
-// NewPTFComposer returns a new Composer that supports composing resources using
-// both Patch and Transform (P&T) logic and a pipeline of Composition Functions.
-func NewPTFComposer(kube client.Client, o ...PTFComposerOption) *PTFComposer {
-	// TODO(negz): Can we avoid double-wrapping if the supplied client is
-	// already wrapped? Or just do away with unstructured.NewClient completely?
-	kube = unstructured.NewClient(kube)
-
-	f := NewSecretConnectionDetailsFetcher(kube)
-
-	c := &PTFComposer{
-		client: resource.ClientApplicator{Client: kube, Applicator: resource.NewAPIPatchingApplicator(kube)},
-
-		composite: ptfComposite{
-			ConnectionDetailsFetcher: f,
-			ComposedResourceGetter:   NewExistingComposedResourceGetter(kube, f),
-			ComposedResourceDeleter:  NewUndesiredComposedResourceDeleter(kube),
-			ComposedResourceObserver: ComposedResourceObserverChain{
-				NewConnectionDetailsObserver(ConnectionDetailsExtractorFn(ExtractConnectionDetails)),
-				NewReadinessObserver(ReadinessCheckerFn(IsReady)),
-			},
-		},
-		composition: ptfComposition{
-			PatchAndTransformer:    NewXRCDPatchAndTransformer(RendererFn(RenderComposite), NewAPIDryRunRenderer(kube)),
-			FunctionPipelineRunner: NewFunctionPipeline(ContainerFunctionRunnerFn(RunFunction)),
-		},
-	}
-
-	for _, fn := range o {
-		fn(c)
-	}
-
-	return c
-}
-
-// PTFCompositionState is used throughout the PTFComposer to track its state.
-type PTFCompositionState struct {
-	Composite         resource.Composite
-	ConnectionDetails managed.ConnectionDetails
-	ComposedResources ComposedResourceStates
-	Events            []event.Event
-}
-
-// Compose resources using both either the Patch & Transform style resources
-// array, the functions array, or both.
-func (c *PTFComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) { //nolint:gocyclo // We probably don't want any further abstraction for the sake of reduced complexity.
-	xc, err := c.composite.FetchConnection(ctx, xr)
-	if err != nil {
-		return CompositionResult{}, errors.Wrap(err, errFetchXRConnectionDetails)
-	}
-
-	cds, err := c.composite.GetComposedResources(ctx, xr)
-	if err != nil {
-		return CompositionResult{}, errors.Wrap(err, errGetExistingCDs)
-	}
-
-	state := &PTFCompositionState{
-		Composite:         xr,
-		ConnectionDetails: xc,
-		ComposedResources: cds,
-		Events:            make([]event.Event, 0),
-	}
-
-	// Build observed state to be passed to our Composition Function pipeline.
-	// Doing this before we patch and transform ensures we report the state we
-	// actually observed before we made any mutations.
-	o, err := FunctionIOObserved(state)
-	if err != nil {
-		return CompositionResult{}, errors.Wrap(err, errBuildFunctionIOObserved)
-	}
-
-	// Run P&T logic, updating the composition state accordingly.
-	if err := c.composition.PatchAndTransform(ctx, req, state); err != nil {
-		return CompositionResult{}, errors.Wrap(err, errPatchAndTransform)
-	}
-
-	// Build the initial desired state to be passed to our Composition Function
-	// pipeline. It's expected that each function in the pipeline will mutate
-	// this state. It includes any desired state accumulated by the P&T logic.
-	d, err := FunctionIODesired(state)
-	if err != nil {
-		return CompositionResult{}, errors.Wrap(err, errBuildFunctionIODesired)
-	}
-
-	// Run Composition Functions, updating the composition state accordingly.
-	// Note that this will replace state.Composite with a new object that was
-	// unmarshalled from the function pipeline's desired state.
-	if err := c.composition.RunFunctionPipeline(ctx, req, state, o, d); err != nil {
-		return CompositionResult{}, errors.Wrap(err, errRunFunctionPipeline)
-	}
-
-	// Garbage collect any resources that aren't part of our final desired
-	// state. We must do this before we update the XR's resource references to
-	// ensure that we don't forget and leak them if a delete fails.
-	if err := c.composite.DeleteComposedResources(ctx, state); err != nil {
-		return CompositionResult{}, errors.Wrap(err, errDeleteUndesiredCDs)
-	}
-
-	// Record references to all desired composed resources.
-	UpdateResourceRefs(state)
-
-	// The supplied options ensure we merge rather than replace arrays and
-	// objects for which a merge configuration has been specified.
-	//
-	// Note that at this point state.Composite should be a new object - not the
-	// xr that was passed to this Compose method. If this call to Apply changes
-	// the XR in the API server (i.e. if it's not a no-op) the xr object that
-	// was passed to this method will have a stale meta.resourceVersion. This
-	// Subsequent attempts to update that object will therefore fail. This
-	// should be okay; the caller should keep trying until this is a no-op.
-	ao := mergeOptions(filterPatches(allPatches(state.ComposedResources), patchTypesToXR()...))
-	if err := c.client.Apply(ctx, state.Composite, ao...); err != nil {
-		return CompositionResult{}, errors.Wrap(err, errApplyXR)
-	}
-
-	// We apply all of our composed resources before we observe them and update
-	// in the loop below. This ensures that issues observing and processing one
-	// composed resource won't block the application of another.
-	for _, cd := range state.ComposedResources {
-		// Don't try to apply this resource if we didn't render it successfully
-		// during Patch & Transform Composition. It's possible that cd.Resource
-		// is in a partially rendered state. This would be particularly bad if
-		// in that state it could be created successfully, but could not later
-		// be updated to its fully rendered desired state. Note that this
-		// doesn't mean this resource won't exist; it might have been created
-		// previously.
-		if cd.TemplateRenderErr != nil {
-			continue
-		}
-
-		ao := []resource.ApplyOption{resource.MustBeControllableBy(state.Composite.GetUID()), usage.RespectOwnerRefs()}
-		if cd.Template != nil {
-			ao = append(ao, mergeOptions(filterPatches(cd.Template.Patches, patchTypesFromXR()...))...)
-		}
-		if err := c.client.Apply(ctx, cd.Resource, ao...); err != nil {
-			return CompositionResult{}, errors.Wrapf(err, errFmtApplyCD, cd.ResourceName)
-		}
-	}
-
-	// Observe all existing composed resources. This derives the XR's connection
-	// details from those of the composed resources. It also runs any readiness
-	// checks found in either P&T templates or the FunctionIO desired state.
-	if err := c.composite.ObserveComposedResources(ctx, state); err != nil {
-		return CompositionResult{}, errors.Wrap(err, "cannot observe composed resources")
-	}
-
-	out := make([]ComposedResource, 0, len(state.ComposedResources))
-	for _, cd := range state.ComposedResources {
-		out = append(out, cd.ComposedResource)
-	}
-
-	return CompositionResult{ConnectionDetails: state.ConnectionDetails, Composed: out, Events: state.Events}, nil
-}
-
-func allPatches(cds ComposedResourceStates) []v1.Patch {
-	out := make([]v1.Patch, 0, len(cds))
-	for _, cd := range cds {
-		if cd.Template == nil {
-			continue
-		}
-		out = append(out, cd.Template.Patches...)
-	}
-	return out
-}
-
-// An ExistingComposedResourceGetter uses an XR's resource references to load
-// any existing composed resources from the API server. It also loads their
-// connection details.
-type ExistingComposedResourceGetter struct {
-	resource client.Reader
-	details  managed.ConnectionDetailsFetcher
-}
-
-// NewExistingComposedResourceGetter returns a ComposedResourceGetter that
-// fetches an XR's existing composed resources.
-func NewExistingComposedResourceGetter(c client.Reader, f managed.ConnectionDetailsFetcher) *ExistingComposedResourceGetter {
-	return &ExistingComposedResourceGetter{resource: c, details: f}
-}
-
-// GetComposedResources begins building composed resource state by
-// fetching any existing composed resources referenced by the supplied composite
-// resource, as well as their connection details.
-func (g *ExistingComposedResourceGetter) GetComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-	cds := ComposedResourceStates{}
-
-	for _, ref := range xr.GetResourceReferences() {
-		// The PTComposer writes references to resources that it didn't actually
-		// render or create. It has to create these placeholder refs because it
-		// supports anonymous (unnamed) resource templates; it needs to be able
-		// associate entries a Composition's spec.resources array with entries
-		// in an XR's spec.resourceRefs array by their index. These references
-		// won't have a name - we won't be able to get them because they don't
-		// reference a resource that actually exists.
-		if ref.Name == "" {
-			continue
-		}
-
-		r := composed.New(composed.FromReference(ref))
-		nn := types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}
-		err := g.resource.Get(ctx, nn, r)
-		if kerrors.IsNotFound(err) {
-			// We believe we created this resource, but it doesn't exist.
-			continue
-		}
-		if err != nil {
-			return nil, errors.Wrap(err, errGetComposed)
-		}
-
-		if c := metav1.GetControllerOf(r); c != nil && c.UID != xr.GetUID() {
-			// If we don't control this resource we just pretend it doesn't
-			// exist. We might try to render and re-create it later, but that
-			// should fail because we check the controller ref there too.
-			continue
-		}
-
-		name := GetCompositionResourceName(r)
-		if name == "" {
-			return nil, errors.New(errAnonymousCD)
-		}
-
-		conn, err := g.details.FetchConnection(ctx, r)
-		if err != nil {
-			return nil, errors.Wrapf(err, errFmtFetchCDConnectionDetails, name, r.GetKind(), r.GetName())
-		}
-
-		cds.Merge(ComposedResourceState{
-			ComposedResource:  ComposedResource{ResourceName: name},
-			Resource:          r,
-			ConnectionDetails: conn,
-		})
-	}
-
-	return cds, nil
-}
-
-// FunctionIOObserved builds observed state for a FunctionIO from the XR and any
-// existing composed resources. This reflects the observed state of the world
-// before any Composition (P&T or function-based) has taken place.
-func FunctionIOObserved(s *PTFCompositionState) (iov1alpha1.Observed, error) {
-	raw, err := json.Marshal(s.Composite)
-	if err != nil {
-		return iov1alpha1.Observed{}, errors.Wrap(err, errMarshalXR)
-	}
-
-	rs := runtime.RawExtension{Raw: raw}
-	econn := make([]iov1alpha1.ExplicitConnectionDetail, 0, len(s.ConnectionDetails))
-	for n, v := range s.ConnectionDetails {
-		econn = append(econn, iov1alpha1.ExplicitConnectionDetail{Name: n, Value: string(v)})
-	}
-
-	oxr := iov1alpha1.ObservedComposite{Resource: rs, ConnectionDetails: econn}
-
-	ocds := make([]iov1alpha1.ObservedResource, 0, len(s.ComposedResources))
-	for _, cd := range s.ComposedResources {
-
-		raw, err := json.Marshal(cd.Resource)
-		if err != nil {
-			return iov1alpha1.Observed{}, errors.Wrap(err, errMarshalCD)
-		}
-
-		rs := runtime.RawExtension{Raw: raw}
-
-		ecds := make([]iov1alpha1.ExplicitConnectionDetail, 0, len(cd.ConnectionDetails))
-		for n, v := range cd.ConnectionDetails {
-			ecds = append(ecds, iov1alpha1.ExplicitConnectionDetail{Name: n, Value: string(v)})
-		}
-
-		ocds = append(ocds, iov1alpha1.ObservedResource{
-			Name:              cd.ResourceName,
-			Resource:          rs,
-			ConnectionDetails: ecds,
-		})
-	}
-
-	return iov1alpha1.Observed{Composite: oxr, Resources: ocds}, nil
-}
-
-// An XRCDPatchAndTransformer runs a Composition's Patches & Transforms against
-// both the XR and composed resources.
-type XRCDPatchAndTransformer struct {
-	composite Renderer
-	composed  Renderer
-}
-
-// NewXRCDPatchAndTransformer returns a PatchAndTransformer that runs Patches
-// and Transforms against both the XR and composed resources.
-func NewXRCDPatchAndTransformer(composite, composed Renderer) *XRCDPatchAndTransformer {
-	return &XRCDPatchAndTransformer{composite: composite, composed: composed}
-}
-
-// PatchAndTransform updates the supplied composition state by running all
-// patches and transforms within the CompositionRequest.
-func (pt *XRCDPatchAndTransformer) PatchAndTransform(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-	// Inline PatchSets before composing resources.
-	ct, err := ComposedTemplates(req.Revision.Spec.PatchSets, req.Revision.Spec.Resources)
-	if err != nil {
-		return errors.Wrap(err, errInline)
-	}
-
-	// If we have an environment, run all environment patches before composing
-	// resources.
-	if req.Environment != nil && req.Revision.Spec.Environment != nil {
-		for i, p := range req.Revision.Spec.Environment.Patches {
-			if err := ApplyEnvironmentPatch(p, s.Composite, req.Environment); err != nil {
-				return errors.Wrapf(err, errFmtPatchEnvironment, i)
-			}
-		}
-	}
-
-	// Render composite and composed resources using any P&T resource templates.
-	// Note that we require templates to be named; a CompositionValidator should
-	// enforce this.
-	for i := range ct {
-		t := ct[i]
-
-		var r resource.Composed = composed.New()
-
-		// Templates must be named. This is a requirement to use Composition
-		// Functions and thus this Composer implementation.
-		if cd, exists := s.ComposedResources[*t.Name]; exists {
-			r = cd.Resource
-
-			// Typically we'll patch from composed resource status to the XR so
-			// we only want to render (i.e. patch) the XR from composed
-			// resources that actually exist.
-			if err := pt.composite.Render(ctx, s.Composite, r, t, req.Environment); err != nil {
-				// TODO(negz): Why is it that an error rendering composed->XR is
-				// terminal, but an error rendering XR->composed is not?
-				return errors.Wrapf(err, errFmtRenderXR, *t.Name, r.GetObjectKind().GroupVersionKind().Kind, r.GetName())
-			}
-		}
-
-		rerr := pt.composed.Render(ctx, s.Composite, r, t, req.Environment)
-		if rerr != nil {
-			// Failures to patch from XR->composed aren't terminal. It could be
-			// that other resources need to patch the XR in order for the fields
-			// this render wants to patch from to exist. Rather than returning
-			// this error we just set Rendered = false in our state and return
-			// a Warning event describing what happened.
-			s.Events = append(s.Events, event.Warning(reasonCompose, errors.Wrapf(rerr, errFmtResourceName, *t.Name)))
-		}
-
-		s.ComposedResources.Merge(ComposedResourceState{
-			ComposedResource:  ComposedResource{ResourceName: *t.Name},
-			Resource:          r,
-			Template:          &t,
-			TemplateRenderErr: rerr,
-		})
-	}
-	return nil
-}
-
-// FunctionIODesired builds the initial desired state for a FunctionIO from the XR
-// and any existing or impending composed resources. This reflects the observed
-// state of the world plus the initial desired state as built up by any P&T
-// Composition that has taken place.
-func FunctionIODesired(s *PTFCompositionState) (iov1alpha1.Desired, error) {
-	raw, err := json.Marshal(s.Composite)
-	if err != nil {
-		return iov1alpha1.Desired{}, errors.Wrap(err, errMarshalXR)
-	}
-
-	rs := runtime.RawExtension{Raw: raw}
-	econn := make([]iov1alpha1.ExplicitConnectionDetail, 0, len(s.ConnectionDetails))
-	for n, v := range s.ConnectionDetails {
-		econn = append(econn, iov1alpha1.ExplicitConnectionDetail{Name: n, Value: string(v)})
-	}
-
-	dxr := iov1alpha1.DesiredComposite{Resource: rs, ConnectionDetails: econn}
-
-	dcds := make([]iov1alpha1.DesiredResource, 0, len(s.ComposedResources))
-	for _, cd := range s.ComposedResources {
-		if cd.Template == nil {
-			// This composed resource isn't associated with a template. It must
-			// be an existing resource that isn't desired by P&T Composition.
-			// Perhaps it's only desired by the function pipeline, or was
-			// desired by P&T but now isn't.
-			continue
-		}
-		raw, err := json.Marshal(cd.Resource)
-		if err != nil {
-			return iov1alpha1.Desired{}, errors.Wrap(err, errMarshalCD)
-		}
-		dcds = append(dcds, iov1alpha1.DesiredResource{
-			Name:     cd.ResourceName,
-			Resource: runtime.RawExtension{Raw: raw},
-
-			// TODO(negz): Should we include any connection details and
-			// readiness checks from the P&T templates here? Doing so would
-			// allow the composition function pipeline to alter them - i.e. to
-			// remove details/checks. Currently the two are additive - we take
-			// all the connection detail extraction configs and readiness checks
-			// from the P&T process then append any from the function process.
-		})
-	}
-
-	return iov1alpha1.Desired{Composite: dxr, Resources: dcds}, nil
-}
-
-// A ContainerFunctionRunnerOption modifies the behavior of a
-// ContainerFunctionRunner by mutating the supplied RunFunctionRequest.
-type ContainerFunctionRunnerOption func(ctx context.Context, fn *v1.ContainerFunction, r *fnv1alpha1.RunFunctionRequest) error
-
-// WithKubernetesAuthentication configures a ContainerFunctionRunner to use
-// "Kubernetes" authentication to pull images from a private OCI registry.
-// Kubernetes authentication emulates how the Kubelet pulls private images.
-// Specifically, it:
-//
-// 1. Loads credentials from the Docker config file, if any.
-// 2. Loads credentials from the supplied service account's image pull secrets.
-// 3. Loads credentials from the function's image pull secrets.
-// 4. Loads credentials using the GKE, EKS, or AKS credentials helper.
-func WithKubernetesAuthentication(c client.Reader, namespace, serviceAccount, registry string) ContainerFunctionRunnerOption {
-	return func(ctx context.Context, fn *v1.ContainerFunction, r *fnv1alpha1.RunFunctionRequest) error {
-
-		sa := &corev1.ServiceAccount{}
-		if err := c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: serviceAccount}, sa); err != nil {
-			return errors.Wrap(err, errGetServiceAccount)
-		}
-
-		refs := make([]corev1.LocalObjectReference, 0, len(fn.ImagePullSecrets)+len(sa.ImagePullSecrets))
-		refs = append(refs, sa.ImagePullSecrets...)
-		refs = append(refs, fn.ImagePullSecrets...)
-
-		ips := make([]corev1.Secret, len(refs))
-		for i := range refs {
-			if err := c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: refs[i].Name}, &ips[i]); err != nil {
-				return errors.Wrap(err, errGetImagePullSecret)
-			}
-		}
-
-		// We use NewFromPullSecrets rather than NewInCluster so that we can use
-		// our own client.Reader rather than the default in-cluster client. This
-		// makes the option easier to test, ensures we re-use any existing
-		// client caches, and allows us to run out-of-cluster.
-		keychain, err := k8schain.NewFromPullSecrets(ctx, ips)
-		if err != nil {
-			return errors.Wrap(err, errNewKeychain)
-		}
-
-		ref, err := name.ParseReference(fn.Image, name.WithDefaultRegistry(registry))
-		if err != nil {
-			return errors.Wrap(err, errParseImage)
-		}
-		auth, err := keychain.Resolve(ref.Context())
-		if err != nil {
-			return errors.Wrap(err, errResolveKeychain)
-		}
-		a, err := auth.Authorization()
-		if err != nil {
-			return errors.Wrap(err, errAuthCfg)
-		}
-
-		if r.ImagePullConfig == nil {
-			r.ImagePullConfig = &fnv1alpha1.ImagePullConfig{}
-		}
-
-		r.ImagePullConfig.Auth = &fnv1alpha1.ImagePullAuth{
-			Username:      a.Username,
-			Password:      a.Password,
-			Auth:          a.Auth,
-			IdentityToken: a.IdentityToken,
-			RegistryToken: a.RegistryToken,
-		}
-
-		return nil
-	}
-}
-
-// A ContainerFunctionRunner runs a containerized Composition Function.
-type ContainerFunctionRunner interface {
-	RunFunction(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error)
-}
-
-// A ContainerFunctionRunnerFn runs a containerized Composition Function.
-type ContainerFunctionRunnerFn func(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error)
-
-// RunFunction runs a containerized Composition Function.
-func (fn ContainerFunctionRunnerFn) RunFunction(ctx context.Context, fnio *iov1alpha1.FunctionIO, fnc *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error) {
-	return fn(ctx, fnio, fnc, o...)
-}
-
-// A FunctionPipeline runs a pipeline of Composition Functions.
-type FunctionPipeline struct {
-	container     ContainerFunctionRunner
-	containerOpts []ContainerFunctionRunnerOption
-}
-
-// NewFunctionPipeline returns a FunctionPipeline that runs functions using the
-// supplied ContainerFunctionRunner.
-func NewFunctionPipeline(c ContainerFunctionRunner, o ...ContainerFunctionRunnerOption) *FunctionPipeline {
-	return &FunctionPipeline{container: c, containerOpts: o}
-}
-
-// RunFunctionPipeline runs a pipeline of Composition Functions.
-func (p *FunctionPipeline) RunFunctionPipeline(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error { //nolint:gocyclo // Currently only at 12.
-	r := make([]iov1alpha1.Result, 0)
-	for _, fn := range req.Revision.Spec.Functions {
-		switch fn.Type {
-		case v1.FunctionTypeContainer:
-			fnio, err := p.container.RunFunction(ctx, &iov1alpha1.FunctionIO{Config: fn.Config, Observed: o, Desired: d, Results: r}, fn.Container, p.containerOpts...)
-			if err != nil {
-				return errors.Wrapf(err, errFmtRunFn, fn.Name)
-			}
-			// We require each function to pass through any results and desired
-			// state from previous functions in the pipeline that they're
-			// unconcerned with, as well as their own results and desired state.
-			// We pass all functions the same observed state, since it should
-			// represent the state before the function pipeline started.
-			d = fnio.Desired
-			r = fnio.Results
-		default:
-			return errors.Wrapf(errors.Errorf(errFmtUnsupportedFnType, fn.Type), errFmtRunFn, fn.Name)
-		}
-	}
-
-	// Results of fatal severity stop the Composition process. Normal or warning
-	// results are accumulated to be emitted as events by the Reconciler.
-	for _, rs := range r {
-		switch rs.Severity {
-		case iov1alpha1.SeverityFatal:
-			return errors.Wrap(errors.New(rs.Message), errFatalResult)
-		case iov1alpha1.SeverityWarning:
-			s.Events = append(s.Events, event.Warning(reasonCompose, errors.New(rs.Message)))
-		case iov1alpha1.SeverityNormal:
-			s.Events = append(s.Events, event.Normal(reasonCompose, rs.Message))
-		}
-	}
-
-	u := &kunstructured.Unstructured{}
-	if err := json.Unmarshal(d.Composite.Resource.Raw, u); err != nil {
-		return errors.Wrap(err, errUnmarshalDesiredXR)
-	}
-	s.Composite = &composite.Unstructured{Unstructured: *u}
-
-	s.ConnectionDetails = managed.ConnectionDetails{}
-	for _, cd := range d.Composite.ConnectionDetails {
-		s.ConnectionDetails[cd.Name] = []byte(cd.Value)
-	}
-
-	for _, dr := range d.Resources {
-		cd, err := ParseDesiredResource(dr, s.Composite)
-		if err != nil {
-			return errors.Wrapf(err, errFmtParseDesiredCD, dr.Name)
-		}
-
-		s.ComposedResources.Merge(cd)
-	}
-
-	return nil
-}
-
-// RunFunction calls an external container function runner via gRPC.
-func RunFunction(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error) {
-	in, err := yaml.Marshal(fnio)
-	if err != nil {
-		return nil, errors.Wrap(err, errMarshalFnIO)
-	}
-
-	target := DefaultTarget
-	if fn.Runner != nil && fn.Runner.Endpoint != nil {
-		target = *fn.Runner.Endpoint
-	}
-
-	conn, err := grpc.DialContext(ctx, target, grpc.WithTransportCredentials(insecure.NewCredentials()))
-	if err != nil {
-		return nil, errors.Wrap(err, errDialRunner)
-	}
-	// Remember to close the connection, we are not deferring it to be able to properly handle errors,
-	// without having to use a named return.
-
-	req := &fnv1alpha1.RunFunctionRequest{
-		Image:             fn.Image,
-		Input:             in,
-		ImagePullConfig:   ImagePullConfig(fn),
-		RunFunctionConfig: RunFunctionConfig(fn),
-	}
-
-	for _, opt := range o {
-		if err := opt(ctx, fn, req); err != nil {
-			_ = conn.Close()
-			return nil, errors.Wrap(err, errApplyRunFunctionOption)
-		}
-	}
-
-	rsp, err := fnv1alpha1.NewContainerizedFunctionRunnerServiceClient(conn).RunFunction(ctx, req)
-	if err != nil {
-		// TODO(negz): Parse any gRPC status codes.
-		_ = conn.Close()
-		return nil, errors.Wrap(err, errRunFnContainer)
-	}
-
-	if err := conn.Close(); err != nil {
-		return nil, errors.Wrap(err, errCloseRunner)
-	}
-
-	// TODO(negz): Sanity check this FunctionIO to ensure the function returned
-	// a valid response. Does it contain at least a desired Composite resource?
-	out := &iov1alpha1.FunctionIO{}
-	return out, errors.Wrap(yaml.Unmarshal(rsp.Output, out), errUnmarshalFnIO)
-}
-
-// ParseDesiredResource parses a (composed) DesiredResource from a FunctionIO.
-// It adds some labels and annotations that are required for Crossplane to track
-// the composed resources, but otherwise tries to be relatively unopinionated.
-// It does not for example automatically generate a name for the composed
-// resource; the Composition Function must do so.
-func ParseDesiredResource(dr iov1alpha1.DesiredResource, owner resource.Object) (ComposedResourceState, error) {
-	u := &kunstructured.Unstructured{}
-	if err := json.Unmarshal(dr.Resource.Raw, u); err != nil {
-		return ComposedResourceState{}, errors.Wrap(err, errUnmarshalDesiredCD)
-	}
-
-	r := &composed.Unstructured{Unstructured: *u}
-
-	// Annotate the desired resource so we know which P&T resource template
-	// and/or FunctionIO desired.resources entry it is associated with.
-	SetCompositionResourceName(r, dr.Name)
-
-	meta.AddLabels(r, map[string]string{
-		xcrd.LabelKeyNamePrefixForComposed: owner.GetLabels()[xcrd.LabelKeyNamePrefixForComposed],
-		xcrd.LabelKeyClaimName:             owner.GetLabels()[xcrd.LabelKeyClaimName],
-		xcrd.LabelKeyClaimNamespace:        owner.GetLabels()[xcrd.LabelKeyClaimNamespace],
-	})
-
-	// Ensure our XR is the controller of the resource.
-	ref := meta.TypedReferenceTo(owner, owner.GetObjectKind().GroupVersionKind())
-	if err := meta.AddControllerReference(r, meta.AsController(ref)); err != nil {
-		return ComposedResourceState{}, errors.Wrap(err, errSetControllerRef)
-	}
-
-	cd := ComposedResourceState{
-		ComposedResource: ComposedResource{ResourceName: dr.Name},
-		Resource:         r,
-		Desired:          &dr,
-	}
-	return cd, nil
-}
-
-// ImagePullConfig builds an ImagePullConfig for a FunctionIO.
-func ImagePullConfig(fn *v1.ContainerFunction) *fnv1alpha1.ImagePullConfig {
-	cfg := &fnv1alpha1.ImagePullConfig{}
-
-	if fn.ImagePullPolicy != nil {
-		switch *fn.ImagePullPolicy {
-		case corev1.PullAlways:
-			cfg.PullPolicy = fnv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_ALWAYS
-		case corev1.PullNever:
-			cfg.PullPolicy = fnv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_NEVER
-		case corev1.PullIfNotPresent:
-			fallthrough
-		default:
-			cfg.PullPolicy = fnv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_IF_NOT_PRESENT
-		}
-	}
-	return cfg
-}
-
-// RunFunctionConfig builds a RunFunctionConfig for a FunctionIO.
-func RunFunctionConfig(fn *v1.ContainerFunction) *fnv1alpha1.RunFunctionConfig {
-	out := &fnv1alpha1.RunFunctionConfig{}
-	if fn.Timeout != nil {
-		out.Timeout = durationpb.New(fn.Timeout.Duration)
-	}
-	if fn.Resources != nil {
-		out.Resources = &fnv1alpha1.ResourceConfig{}
-		if fn.Resources.Limits != nil {
-			out.Resources.Limits = &fnv1alpha1.ResourceLimits{}
-			if fn.Resources.Limits.CPU != nil {
-				out.Resources.Limits.Cpu = fn.Resources.Limits.CPU.String()
-			}
-			if fn.Resources.Limits.Memory != nil {
-				out.Resources.Limits.Memory = fn.Resources.Limits.Memory.String()
-			}
-		}
-	}
-	if fn.Network != nil {
-		out.Network = &fnv1alpha1.NetworkConfig{}
-		if fn.Network.Policy != nil {
-			switch *fn.Network.Policy {
-			case v1.ContainerFunctionNetworkPolicyIsolated:
-				out.Network.Policy = fnv1alpha1.NetworkPolicy_NETWORK_POLICY_ISOLATED
-			case v1.ContainerFunctionNetworkPolicyRunner:
-				out.Network.Policy = fnv1alpha1.NetworkPolicy_NETWORK_POLICY_RUNNER
-			}
-		}
-	}
-	return out
-}
-
-// An UndesiredComposedResourceDeleter deletes composed resources from the API
-// server and from Composition state if their state doesn't include a FunctionIO
-// desired resource. This indicates the composed resource either exists or was
-// going to be created by P&T composition, but didn't survive the Composition
-// Function pipeline.
-type UndesiredComposedResourceDeleter struct {
-	client client.Writer
-}
-
-// NewUndesiredComposedResourceDeleter returns a ComposedResourceDeleter that
-// deletes undesired composed resources from both the API server and Composition
-// state.
-func NewUndesiredComposedResourceDeleter(c client.Writer) *UndesiredComposedResourceDeleter {
-	return &UndesiredComposedResourceDeleter{client: c}
-}
-
-// DeleteComposedResources deletes any composed resource that didn't come out the other
-// end of the Composition Function pipeline (i.e. that wasn't in the final
-// desired state after running the pipeline). Composed resources are deleted
-// from both the supposed composition state and from the API server.
-func (d *UndesiredComposedResourceDeleter) DeleteComposedResources(ctx context.Context, s *PTFCompositionState) error {
-	for name, cd := range s.ComposedResources {
-		// We know this resource is still desired because we recorded its
-		// desired state after running the FunctionIO pipeline. Don't garbage
-		// collect it.
-		if cd.Desired != nil {
-			continue
-		}
-
-		// We don't desire this resource to exist; remove it from our state.
-		delete(s.ComposedResources, name)
-
-		// No need to garbage collect resources that don't exist.
-		if !meta.WasCreated(cd.Resource) {
-			continue
-		}
-
-		// We want to garbage collect this resource, but we don't control it.
-		if c := metav1.GetControllerOf(cd.Resource); c == nil || c.UID != s.Composite.GetUID() {
-			continue
-		}
-
-		if err := d.client.Delete(ctx, cd.Resource); resource.IgnoreNotFound(err) != nil {
-			return errors.Wrapf(err, errFmtDeleteCD, cd.ResourceName, cd.Resource.GetObjectKind().GroupVersionKind().Kind, cd.Resource.GetName())
-		}
-	}
-
-	return nil
-}
-
-// UpdateResourceRefs updates the supplied state to ensure the XR references all
-// composed resources that exist or are pending creation.
-func UpdateResourceRefs(s *PTFCompositionState) {
-	refs := make([]corev1.ObjectReference, 0, len(s.ComposedResources))
-	for _, cd := range s.ComposedResources {
-		// Don't record references to resources that don't exist and that failed
-		// to render. We won't apply (i.e. create) these resources this time
-		// around, so there's no need to create dangling references to them.
-		if !meta.WasCreated(cd.Resource) && cd.TemplateRenderErr != nil {
-			continue
-		}
-		ref := meta.ReferenceTo(cd.Resource, cd.Resource.GetObjectKind().GroupVersionKind())
-		refs = append(refs, *ref)
-	}
-
-	// We want to ensure our refs are stable.
-	sort.Slice(refs, func(i, j int) bool {
-		ri, rj := refs[i], refs[j]
-		return ri.APIVersion+ri.Kind+ri.Name < rj.APIVersion+rj.Kind+rj.Name
-	})
-
-	s.Composite.SetResourceReferences(refs)
-}
-
-// A ComposedResourceObserverChain runs a slice of ComposedResourceObservers.
-type ComposedResourceObserverChain []ComposedResourceObserver
-
-// ObserveComposedResources runs the slice of ComposedResourceObservers.
-func (o ComposedResourceObserverChain) ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error {
-	for _, cro := range o {
-		if err := cro.ObserveComposedResources(ctx, s); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// A ReadinessObserver observes composed resource state and updates it to
-// indicate whether each composed resource is ready per the readiness checks
-// associated with each resource, which are derived from their P&T resource
-// template and/or Composition Function desired state.
-type ReadinessObserver struct {
-	check ReadinessChecker
-}
-
-// NewReadinessObserver returns a ComposedResourceObserver that observes whether
-// composed resources are ready.
-func NewReadinessObserver(c ReadinessChecker) *ReadinessObserver {
-	return &ReadinessObserver{check: c}
-}
-
-// ObserveComposedResources to determine their readiness.
-func (o *ReadinessObserver) ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error {
-	for _, cd := range s.ComposedResources {
-		rcfgs := append(ReadinessChecksFromComposedTemplate(cd.Template), ReadinessChecksFromDesiredResource(cd.Desired)...)
-		ready, err := o.check.IsReady(ctx, cd.Resource, rcfgs...)
-		if err != nil {
-			return errors.Wrapf(err, errFmtReadiness, cd.ResourceName, cd.Resource.GetObjectKind().GroupVersionKind().Kind, cd.Resource.GetName())
-		}
-
-		s.ComposedResources.Merge(ComposedResourceState{
-			ComposedResource: ComposedResource{
-				ResourceName: cd.ResourceName,
-				Ready:        ready,
-			},
-		})
-	}
-
-	return nil
-}
-
-// A ConnectionDetailsObserver extracts XR connection details from composed
-// resource state. The details to extract are derived from each composed
-// resource's P&T resource template and/or Composition Function desired state.
-type ConnectionDetailsObserver struct {
-	details ConnectionDetailsExtractor
-}
-
-// NewConnectionDetailsObserver returns a ComposedResourceObserver that observes
-// composed resources in order to extract XR connection details.
-func NewConnectionDetailsObserver(e ConnectionDetailsExtractor) *ConnectionDetailsObserver {
-	return &ConnectionDetailsObserver{details: e}
-}
-
-// ObserveComposedResources to extract XR connection details.
-func (o *ConnectionDetailsObserver) ObserveComposedResources(_ context.Context, s *PTFCompositionState) error {
-	for _, cd := range s.ComposedResources {
-		ecfgs := append(ExtractConfigsFromTemplate(cd.Template), ExtractConfigsFromDesired(cd.Desired)...)
-		e, err := o.details.ExtractConnection(cd.Resource, cd.ConnectionDetails, ecfgs...)
-		if err != nil {
-			return errors.Wrapf(err, errFmtExtractConnectionDetails, cd.ResourceName, cd.Resource.GetObjectKind().GroupVersionKind().Kind, cd.Resource.GetName())
-		}
-
-		if s.ConnectionDetails == nil {
-			s.ConnectionDetails = managed.ConnectionDetails{}
-		}
-
-		for key, val := range e {
-			s.ConnectionDetails[key] = val
-		}
-	}
-
-	return nil
-}
diff --git a/internal/controller/apiextensions/composite/composition_render.go b/internal/controller/apiextensions/composite/composition_render.go
new file mode 100644
index 000000000..569300335
--- /dev/null
+++ b/internal/controller/apiextensions/composite/composition_render.go
@@ -0,0 +1,193 @@
+/*
+Copyright 2022 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+*/
+
+package composite
+
+import (
+	"context"
+
+	"k8s.io/apimachinery/pkg/util/json"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/meta"
+	"github.com/crossplane/crossplane-runtime/pkg/resource"
+
+	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
+	"github.com/crossplane/crossplane/internal/xcrd"
+)
+
+// Error strings
+const (
+	errUnmarshalJSON      = "cannot unmarshal JSON data"
+	errMarshalProtoStruct = "cannot marshal protobuf Struct to JSON"
+	errName               = "cannot use dry-run create to name composed resource"
+	errSetControllerRef   = "cannot set controller reference"
+
+	errFmtKindChanged     = "cannot change the kind of a composed resource from %s to %s (possible composed resource template mismatch)"
+	errFmtNamePrefixLabel = "cannot find top-level composite resource name label %q in composite resource metadata"
+
+	// TODO(negz): Include more detail such as field paths if they exist.
+	// Perhaps require each patch type to have a String() method to help
+	// identify it.
+	errFmtPatch = "cannot apply the %q patch at index %d"
+)
+
+// RenderFromJSON renders the supplied resource from JSON bytes.
+func RenderFromJSON(o resource.Object, data []byte) error {
+	gvk := o.GetObjectKind().GroupVersionKind()
+	name := o.GetName()
+	namespace := o.GetNamespace()
+
+	if err := json.Unmarshal(data, o); err != nil {
+		return errors.Wrap(err, errUnmarshalJSON)
+	}
+
+	// TODO(negz): Should we return an error if the name or namespace change,
+	// rather than just silently re-setting it? Presumably these _changing_ is a
+	// sign that something has gone wrong, similar to the GVK changing. What
+	// about the UID changing?
+
+	// Unmarshalling the template will overwrite any existing fields, so we must
+	// restore the existing name, if any.
+	o.SetName(name)
+	o.SetNamespace(namespace)
+
+	// This resource already had a GVK (probably because it already exists), but
+	// when we rendered its template it changed. This shouldn't happen. Either
+	// someone changed the kind in the template or we're trying to use the wrong
+	// template (e.g. because the order of an array of anonymous templates
+	// changed).
+	if !gvk.Empty() && o.GetObjectKind().GroupVersionKind() != gvk {
+		return errors.Errorf(errFmtKindChanged, gvk, o.GetObjectKind().GroupVersionKind())
+	}
+
+	return nil
+}
+
+// RenderFromCompositePatches renders the supplied composed resource by applying
+// all patches that are _from_ the supplied composite resource.
+func RenderFromCompositePatches(cd resource.Composed, xr resource.Composite, p []v1.Patch) error {
+	for i := range p {
+		if err := Apply(p[i], xr, cd, patchTypesFromXR()...); err != nil {
+			return errors.Wrapf(err, errFmtPatch, p[i].Type, i)
+		}
+	}
+	return nil
+}
+
+// RenderToAndFromEnvironmentPatches renders the supplied composed resource by
+// applying all patches that are from or to the supplied environment.
+func RenderToAndFromEnvironmentPatches(cd resource.Composed, e *Environment, p []v1.Patch) error {
+	if e == nil {
+		return nil
+	}
+	for i := range p {
+		if err := ApplyToObjects(p[i], e, cd, patchTypesFromToEnvironment()...); err != nil {
+			return errors.Wrapf(err, errFmtPatch, p[i].Type, i)
+		}
+	}
+	return nil
+}
+
+// RenderToCompositePatches renders the supplied composite resource by applying
+// all patches that are _from_ the supplied composed resource. composed resource
+// and template.
+func RenderToCompositePatches(xr resource.Composite, cd resource.Composed, p []v1.Patch) error {
+	for i := range p {
+		if err := Apply(p[i], xr, cd, patchTypesToXR()...); err != nil {
+			return errors.Wrapf(err, errFmtPatch, p[i].Type, i)
+		}
+	}
+	return nil
+}
+
+// RenderComposedResourceMetadata derives composed resource metadata from the
+// supplied composite resource. It makes the composite resource the controller
+// of the composed resource. It should run toward the end of a render pipeline
+// to ensure that a Composition cannot influence the controller reference.
+func RenderComposedResourceMetadata(cd, xr resource.Object, n ResourceName) error {
+	// Fail early if the supplied composite resource is missing the name prefix
+	// label.
+	if xr.GetLabels()[xcrd.LabelKeyNamePrefixForComposed] == "" {
+		return errors.Errorf(errFmtNamePrefixLabel, xcrd.LabelKeyNamePrefixForComposed)
+	}
+
+	//  We also set generate name in case we
+	// haven't yet named this composed resource.
+	cd.SetGenerateName(xr.GetLabels()[xcrd.LabelKeyNamePrefixForComposed] + "-")
+
+	if n != "" {
+		SetCompositionResourceName(cd, n)
+	}
+
+	meta.AddLabels(cd, map[string]string{
+		xcrd.LabelKeyNamePrefixForComposed: xr.GetLabels()[xcrd.LabelKeyNamePrefixForComposed],
+		xcrd.LabelKeyClaimName:             xr.GetLabels()[xcrd.LabelKeyClaimName],
+		xcrd.LabelKeyClaimNamespace:        xr.GetLabels()[xcrd.LabelKeyClaimNamespace],
+	})
+
+	or := meta.AsController(meta.TypedReferenceTo(xr, xr.GetObjectKind().GroupVersionKind()))
+	return errors.Wrap(meta.AddControllerReference(cd, or), errSetControllerRef)
+}
+
+// TODO(negz): Is this really a 'renderer'? It's simple enough that we should
+// just inline it into the PTComposer, which is now the only consumer.
+
+// A DryRunRenderer performs a dry-run create to validate and name the supplied
+// managed resource.
+type DryRunRenderer interface {
+	DryRunRender(ctx context.Context, cd resource.Object) error
+}
+
+// A DryRunRendererFn is a function that satisfies DryRunRenderer.
+type DryRunRendererFn func(ctx context.Context, cd resource.Object) error
+
+// DryRunRender performs a dry-run create to validate and name the supplied
+// managed resource.
+func (fn DryRunRendererFn) DryRunRender(ctx context.Context, cd resource.Object) error {
+	return fn(ctx, cd)
+}
+
+// An APIDryRunRenderer submits a resource to the API server in order to name
+// and validate it.
+type APIDryRunRenderer struct{ client client.Client }
+
+// NewAPIDryRunRenderer returns a Renderer that submits a resource to
+// the API server in order to name and validate it.
+func NewAPIDryRunRenderer(c client.Client) *APIDryRunRenderer {
+	return &APIDryRunRenderer{client: c}
+}
+
+// DryRunRender submits the resource to the API server via a dry run create in
+// order to name and validate it.
+func (r *APIDryRunRenderer) DryRunRender(ctx context.Context, cd resource.Object) error {
+	// We don't want to dry-run create a resource that can't be named by the API
+	// server due to a missing generate name. We also don't want to create one
+	// that is already named, because doing so will result in an error. The API
+	// server seems to respond with a 500 ServerTimeout error for all dry-run
+	// failures, so we can't just perform a dry-run and ignore 409 Conflicts for
+	// resources that are already named.
+	if cd.GetName() != "" || cd.GetGenerateName() == "" {
+		return nil
+	}
+
+	// The API server returns an available name derived from generateName when
+	// we perform a dry-run create. This name is likely (but not guaranteed) to
+	// be available when we create the composed resource. If the API server
+	// generates a name that is unavailable it will return a 500 ServerTimeout
+	// error.
+	return errors.Wrap(r.client.Create(ctx, cd, client.DryRunAll), errName)
+}
diff --git a/internal/controller/apiextensions/composite/connection.go b/internal/controller/apiextensions/composite/connection.go
index e91709bb2..857e8cda9 100644
--- a/internal/controller/apiextensions/composite/connection.go
+++ b/internal/controller/apiextensions/composite/connection.go
@@ -31,12 +31,12 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
-	iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 )
 
 // Error strings.
 const (
+	errGetSecret      = "cannot get connection secret of composed resource"
 	errConnDetailName = "connection detail is missing name"
 
 	errFmtConnDetailKey  = "connection detail of type %q key is not set"
@@ -271,9 +271,9 @@ type ConnectionDetailExtractConfig struct {
 	Value *string
 }
 
-// ExtractConfigsFromTemplate builds extract configs for the supplied P&T style
-// composed resource template.
-func ExtractConfigsFromTemplate(t *v1.ComposedTemplate) []ConnectionDetailExtractConfig {
+// ExtractConfigsFromComposedTemplate builds extract configs for the supplied
+// P&T style composed resource template.
+func ExtractConfigsFromComposedTemplate(t *v1.ComposedTemplate) []ConnectionDetailExtractConfig {
 	if t == nil {
 		return nil
 	}
@@ -298,33 +298,6 @@ func ExtractConfigsFromTemplate(t *v1.ComposedTemplate) []ConnectionDetailExtrac
 	return out
 }
 
-// ExtractConfigsFromDesired builds extract configs for the supplied Composition
-// Function desired state.
-func ExtractConfigsFromDesired(dr *iov1alpha1.DesiredResource) []ConnectionDetailExtractConfig {
-	if dr == nil {
-		return nil
-	}
-	out := make([]ConnectionDetailExtractConfig, len(dr.ConnectionDetails))
-	for i := range dr.ConnectionDetails {
-		out[i] = ConnectionDetailExtractConfig{
-			Type:                    ConnectionDetailType(dr.ConnectionDetails[i].Type),
-			Value:                   dr.ConnectionDetails[i].Value,
-			FromConnectionSecretKey: dr.ConnectionDetails[i].FromConnectionSecretKey,
-			FromFieldPath:           dr.ConnectionDetails[i].FromFieldPath,
-		}
-
-		if dr.ConnectionDetails[i].Name != nil {
-			out[i].Name = *dr.ConnectionDetails[i].Name
-			continue
-		}
-
-		if out[i].Type == ConnectionDetailTypeFromConnectionSecretKey && out[i].FromConnectionSecretKey != nil {
-			out[i].Name = *out[i].FromConnectionSecretKey
-		}
-	}
-	return out
-}
-
 // Originally there was no 'type' determinator field so Crossplane would infer
 // the type. We maintain this behaviour for backward compatibility when no type
 // is set.
diff --git a/internal/controller/apiextensions/composite/ready.go b/internal/controller/apiextensions/composite/ready.go
index 8284d9c57..fa7f8cd49 100644
--- a/internal/controller/apiextensions/composite/ready.go
+++ b/internal/controller/apiextensions/composite/ready.go
@@ -27,7 +27,6 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
-	iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 )
 
@@ -119,26 +118,6 @@ func ReadinessCheckFromV1(in *v1.ReadinessCheck) ReadinessCheck {
 	return out
 }
 
-// ReadinessCheckFromDesiredReadinessCheck derives a ReadinessCheck from the supplied iov1alpha1.DesiredReadinessCheck.
-func ReadinessCheckFromDesiredReadinessCheck(in *iov1alpha1.DesiredReadinessCheck) ReadinessCheck {
-	if in == nil {
-		return ReadinessCheck{}
-	}
-	out := ReadinessCheck{
-		Type:         ReadinessCheckType(in.Type),
-		FieldPath:    in.FieldPath,
-		MatchString:  in.MatchString,
-		MatchInteger: in.MatchInteger,
-	}
-	if in.MatchCondition != nil {
-		out.MatchCondition = &MatchConditionReadinessCheck{
-			Type:   in.MatchCondition.Type,
-			Status: in.MatchCondition.Status,
-		}
-	}
-	return out
-}
-
 // ReadinessChecksFromComposedTemplate derives readiness checks from the supplied
 // composed template.
 func ReadinessChecksFromComposedTemplate(t *v1.ComposedTemplate) []ReadinessCheck {
@@ -152,19 +131,6 @@ func ReadinessChecksFromComposedTemplate(t *v1.ComposedTemplate) []ReadinessChec
 	return out
 }
 
-// ReadinessChecksFromDesiredResource derives readiness checks from the supplied desired
-// resource.
-func ReadinessChecksFromDesiredResource(dr *iov1alpha1.DesiredResource) []ReadinessCheck {
-	if dr == nil {
-		return nil
-	}
-	out := make([]ReadinessCheck, len(dr.ReadinessChecks))
-	for i := range dr.ReadinessChecks {
-		out[i] = ReadinessCheckFromDesiredReadinessCheck(&dr.ReadinessChecks[i])
-	}
-	return out
-}
-
 // TODO(negz): Ideally we'd validate P&T readiness checks (which are specified
 // in the Composition) using a webhook. We still need to validate the output of
 // a Composition Function Pipeline, though.
diff --git a/internal/controller/apiextensions/composite/reconciler.go b/internal/controller/apiextensions/composite/reconciler.go
index 09c8a582e..63cea2aeb 100644
--- a/internal/controller/apiextensions/composite/reconciler.go
+++ b/internal/controller/apiextensions/composite/reconciler.go
@@ -67,8 +67,6 @@ const (
 	errSelectEnvironment      = "cannot select environment"
 	errCompose                = "cannot compose resources"
 	errRenderCD               = "cannot render composed resource"
-
-	errFmtPatchEnvironment = "cannot apply environment patch at index %d"
 )
 
 // Event reasons.
@@ -182,20 +180,6 @@ func (fn ConfiguratorFn) Configure(ctx context.Context, cr resource.Composite, r
 	return fn(ctx, cr, rev)
 }
 
-// A Renderer is used to render a composed resource.
-type Renderer interface {
-	Render(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error
-}
-
-// A RendererFn may be used to render a composed resource.
-type RendererFn func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error
-
-// Render the supplied composed resource using the supplied composite resource
-// and template as inputs.
-func (fn RendererFn) Render(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-	return fn(ctx, cp, cd, t, env)
-}
-
 // A CompositionRequest is a request to compose resources.
 // It should be treated as immutable.
 type CompositionRequest struct {
@@ -224,6 +208,14 @@ func (fn ComposerFn) Compose(ctx context.Context, xr resource.Composite, req Com
 	return fn(ctx, xr, req)
 }
 
+// A ComposerSelectorFn selects the appropriate Composer for a mode.
+type ComposerSelectorFn func(*v1.CompositionMode) Composer
+
+// Compose calls the Composer returned by calling fn.
+func (fn ComposerSelectorFn) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) {
+	return fn(req.Revision.Spec.Mode).Compose(ctx, xr, req)
+}
+
 // ReconcilerOption is used to configure the Reconciler.
 type ReconcilerOption func(*Reconciler)
 
@@ -630,7 +622,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 	for i, cd := range res.Composed {
 		// Specifying a name for P&T templates is optional but encouraged.
 		// If there was no name, fall back to using the index.
-		id := cd.ResourceName
+		id := string(cd.ResourceName)
 		if id == "" {
 			id = strconv.Itoa(i)
 		}
@@ -652,7 +644,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 		// become ready, since we can't watch them.
 		names := make([]string, len(unready))
 		for i, cd := range unready {
-			names[i] = cd.ResourceName
+			names[i] = string(cd.ResourceName)
 		}
 		// sort for stable condition messages. With functions, we don't have a
 		// stable order otherwise.
diff --git a/internal/controller/apiextensions/controller/options.go b/internal/controller/apiextensions/controller/options.go
index 6f06b4868..caef4d5aa 100644
--- a/internal/controller/apiextensions/controller/options.go
+++ b/internal/controller/apiextensions/controller/options.go
@@ -19,9 +19,11 @@ package controller
 
 import (
 	"github.com/crossplane/crossplane-runtime/pkg/controller"
+
+	"github.com/crossplane/crossplane/internal/xfn"
 )
 
-// Options specific to pkg controllers.
+// Options specific to apiextensions controllers.
 type Options struct {
 	controller.Options
 
@@ -36,4 +38,7 @@ type Options struct {
 	// Registry is the default registry to use when pulling containers for
 	// Composition Functions
 	Registry string
+
+	// FunctionRunner used to run Composition Functions.
+	FunctionRunner *xfn.PackagedFunctionRunner
 }
diff --git a/internal/controller/apiextensions/definition/reconciler.go b/internal/controller/apiextensions/definition/reconciler.go
index 5d4b58908..121b4ee16 100644
--- a/internal/controller/apiextensions/definition/reconciler.go
+++ b/internal/controller/apiextensions/definition/reconciler.go
@@ -486,32 +486,36 @@ func CompositeReconcilerOptions(co apiextensionscontroller.Options, d *v1.Compos
 			composite.WithComposer(composite.NewPTComposer(c, composite.WithComposedConnectionDetailsFetcher(fetcher))))
 	}
 
-	// If Composition Functions are enabled we want to try to use the
-	// PTFComposer. This Composer supports using P&T Composition alone,
-	// Functions alone, or mixing both. It does not support anonymous resource
-	// templates - resource templates with a nil name - because it needs the
-	// name to match entries in the resource templates array to entries in the
-	// FunctionIO used by the templates array. We therefore 'fall back' to the
-	// PTComposer if we encounter a Composition with anonymous templates.
-	// Composition validation ensures that a Composition that uses functions
-	// must have named resources templates.
-	if co.Features.Enabled(features.EnableAlphaCompositionFunctions) {
-		fb := composite.NewFallBackComposer(
-			composite.NewPTFComposer(c,
-				composite.WithComposedResourceGetter(composite.NewExistingComposedResourceGetter(c, fetcher)),
-				composite.WithCompositeConnectionDetailsFetcher(fetcher),
-				composite.WithFunctionPipelineRunner(composite.NewFunctionPipeline(
-					composite.ContainerFunctionRunnerFn(composite.RunFunction),
-					composite.WithKubernetesAuthentication(c, co.Namespace, co.ServiceAccount, co.Registry),
-				)),
-			),
-			composite.NewPTComposer(c, composite.WithComposedConnectionDetailsFetcher(fetcher)),
-			composite.FallBackForAnonymousTemplates(c),
+	// If Composition Functions are enabled we use two different Composer
+	// implementations. One supports P&T (aka 'Resources mode') and the other
+	// Functions (aka 'Pipeline mode').
+	if co.Features.Enabled(features.EnableBetaCompositionFunctions) {
+		ptc := composite.NewPTComposer(c, composite.WithComposedConnectionDetailsFetcher(fetcher))
+
+		fc := composite.NewFunctionComposer(c, co.FunctionRunner,
+			composite.WithComposedResourceObserver(composite.NewExistingComposedResourceObserver(c, fetcher)),
+			composite.WithCompositeConnectionDetailsFetcher(fetcher),
 		)
 
-		// Note that if external secret stores are enabled this will supercede
+		// Note that if external secret stores are enabled this will supersede
 		// the WithComposer option specified in that block.
-		o = append(o, composite.WithComposer(fb))
+		o = append(o, composite.WithComposer(composite.ComposerSelectorFn(func(cm *v1.CompositionMode) composite.Composer {
+			// Resources mode is the implicit default.
+			m := v1.CompositionModeResources
+			if cm != nil {
+				m = *cm
+			}
+			switch m {
+			case v1.CompositionModeResources:
+				return ptc
+			case v1.CompositionModePipeline:
+				return fc
+			default:
+				// This shouldn't be possible, but just in case return the
+				// default Composer.
+				return ptc
+			}
+		})))
 	}
 
 	return o
diff --git a/internal/controller/pkg/controller/options.go b/internal/controller/pkg/controller/options.go
index 6fcd57055..2ca11367c 100644
--- a/internal/controller/pkg/controller/options.go
+++ b/internal/controller/pkg/controller/options.go
@@ -19,7 +19,6 @@ package controller
 
 import (
 	"github.com/crossplane/crossplane-runtime/pkg/controller"
-	"github.com/crossplane/crossplane-runtime/pkg/feature"
 
 	"github.com/crossplane/crossplane/internal/xpkg"
 )
@@ -48,7 +47,4 @@ type Options struct {
 	// so that they can use it to serve webhooks and also the CA bundle will be
 	// injected to CRDs so that API server can make calls to the providers.
 	WebhookTLSSecretName string
-
-	// Features that should be enabled.
-	Features *feature.Flags
 }
diff --git a/internal/controller/pkg/manager/reconciler.go b/internal/controller/pkg/manager/reconciler.go
index 9378c6543..70c0c55ee 100644
--- a/internal/controller/pkg/manager/reconciler.go
+++ b/internal/controller/pkg/manager/reconciler.go
@@ -39,7 +39,7 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
-	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	"github.com/crossplane/crossplane/internal/controller/pkg/controller"
 	"github.com/crossplane/crossplane/internal/xpkg"
 )
@@ -236,10 +236,10 @@ func SetupConfiguration(mgr ctrl.Manager, o controller.Options) error {
 
 // SetupFunction adds a controller that reconciles Functions.
 func SetupFunction(mgr ctrl.Manager, o controller.Options) error {
-	name := "packages/" + strings.ToLower(v1alpha1.FunctionGroupKind)
-	np := func() v1.Package { return &v1alpha1.Function{} }
-	nr := func() v1.PackageRevision { return &v1alpha1.FunctionRevision{} }
-	nrl := func() v1.PackageRevisionList { return &v1alpha1.FunctionRevisionList{} }
+	name := "packages/" + strings.ToLower(v1beta1.FunctionGroupKind)
+	np := func() v1.Package { return &v1beta1.Function{} }
+	nr := func() v1.PackageRevision { return &v1beta1.FunctionRevision{} }
+	nrl := func() v1.PackageRevisionList { return &v1beta1.FunctionRevisionList{} }
 
 	cs, err := kubernetes.NewForConfig(mgr.GetConfig())
 	if err != nil {
@@ -261,8 +261,8 @@ func SetupFunction(mgr ctrl.Manager, o controller.Options) error {
 
 	return ctrl.NewControllerManagedBy(mgr).
 		Named(name).
-		For(&v1alpha1.Function{}).
-		Owns(&v1alpha1.FunctionRevision{}).
+		For(&v1beta1.Function{}).
+		Owns(&v1beta1.FunctionRevision{}).
 		WithOptions(o.ForControllerRuntime()).
 		Complete(ratelimiter.NewReconciler(name, NewReconciler(mgr, opts...), o.GlobalRateLimiter))
 }
diff --git a/internal/controller/pkg/pkg.go b/internal/controller/pkg/pkg.go
index 57fd1b645..e6362997f 100644
--- a/internal/controller/pkg/pkg.go
+++ b/internal/controller/pkg/pkg.go
@@ -24,6 +24,7 @@ import (
 	"github.com/crossplane/crossplane/internal/controller/pkg/manager"
 	"github.com/crossplane/crossplane/internal/controller/pkg/resolver"
 	"github.com/crossplane/crossplane/internal/controller/pkg/revision"
+	"github.com/crossplane/crossplane/internal/features"
 )
 
 // Setup package controllers.
@@ -31,15 +32,26 @@ func Setup(mgr ctrl.Manager, o controller.Options) error {
 	for _, setup := range []func(ctrl.Manager, controller.Options) error{
 		manager.SetupConfiguration,
 		manager.SetupProvider,
-		manager.SetupFunction,
 		resolver.Setup,
 		revision.SetupConfigurationRevision,
 		revision.SetupProviderRevision,
-		revision.SetupFunctionRevision,
 	} {
 		if err := setup(mgr, o); err != nil {
 			return err
 		}
 	}
+
+	// We only want to start the Function controllers if Functions are enabled.
+	if o.Features.Enabled(features.EnableBetaCompositionFunctions) {
+		for _, setup := range []func(ctrl.Manager, controller.Options) error{
+			manager.SetupFunction,
+			revision.SetupFunctionRevision,
+		} {
+			if err := setup(mgr, o); err != nil {
+				return err
+			}
+		}
+	}
+
 	return nil
 }
diff --git a/internal/controller/pkg/resolver/reconciler.go b/internal/controller/pkg/resolver/reconciler.go
index 5a4bb4972..df5a0dd8c 100644
--- a/internal/controller/pkg/resolver/reconciler.go
+++ b/internal/controller/pkg/resolver/reconciler.go
@@ -38,7 +38,6 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
-	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
 	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	"github.com/crossplane/crossplane/internal/controller/pkg/controller"
 	"github.com/crossplane/crossplane/internal/dag"
@@ -272,7 +271,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 	case v1beta1.ProviderPackageType:
 		pack = &v1.Provider{}
 	case v1beta1.FunctionPackageType:
-		pack = &v1alpha1.Function{}
+		pack = &v1beta1.Function{}
 	default:
 		log.Debug(errInvalidPackageType)
 		return reconcile.Result{Requeue: false}, nil
diff --git a/internal/controller/pkg/revision/deployment.go b/internal/controller/pkg/revision/deployment.go
index 40e5a7c06..da5399100 100644
--- a/internal/controller/pkg/revision/deployment.go
+++ b/internal/controller/pkg/revision/deployment.go
@@ -25,9 +25,10 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
-	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	"github.com/crossplane/crossplane/internal/initializer"
 )
 
@@ -40,9 +41,10 @@ var (
 	runAsNonRoot             = true
 )
 
-// Providers are expected to use port 8080 if they expose Prometheus metrics,
-// which any provider built using controller-runtime will do by default.
 const (
+	// Providers are expected to use port 8080 if they expose Prometheus
+	// metrics, which any provider built using controller-runtime will do by
+	// default.
 	promPortName   = "metrics"
 	promPortNumber = 8080
 
@@ -51,9 +53,10 @@ const (
 	webhookTLSCertDir       = "/webhook/tls"
 	webhookPortName         = "webhook"
 
+	// See https://github.com/grpc/grpc/blob/v1.58.0/doc/naming.md
 	grpcPortName       = "grpc"
 	servicePort        = 9443
-	serviceEndpointFmt = "https://%s.%s:%d"
+	serviceEndpointFmt = "dns:///%s.%s:%d"
 
 	essTLSCertDirEnvVar = "ESS_TLS_CERTS_DIR"
 	essCertsVolumeName  = "ess-client-certs"
@@ -230,12 +233,12 @@ func buildProviderDeployment(provider *pkgmetav1.Provider, revision v1.PackageRe
 	return s, d, svc, secSer, secCli
 }
 
-func buildFunctionDeployment(function *pkgmetav1alpha1.Function, revision v1.PackageRevision, cc *v1alpha1.ControllerConfig, namespace string, pullSecrets []corev1.LocalObjectReference) (*corev1.ServiceAccount, *appsv1.Deployment, *corev1.Service, *corev1.Secret) {
+func buildFunctionDeployment(function *pkgmetav1beta1.Function, revision v1.PackageRevision, cc *v1alpha1.ControllerConfig, namespace string, pullSecrets []corev1.LocalObjectReference) (*corev1.ServiceAccount, *appsv1.Deployment, *corev1.Service, *corev1.Secret) {
 	s := &corev1.ServiceAccount{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:            revision.GetName(),
 			Namespace:       namespace,
-			OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(revision, v1alpha1.FunctionRevisionGroupVersionKind))},
+			OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(revision, v1beta1.FunctionRevisionGroupVersionKind))},
 		},
 		ImagePullSecrets: pullSecrets,
 	}
@@ -262,7 +265,7 @@ func buildFunctionDeployment(function *pkgmetav1alpha1.Function, revision v1.Pac
 			Name:      revision.GetName(),
 			Namespace: namespace,
 			OwnerReferences: []metav1.OwnerReference{
-				meta.AsController(meta.TypedReferenceTo(revision, v1alpha1.FunctionRevisionGroupVersionKind)),
+				meta.AsController(meta.TypedReferenceTo(revision, v1beta1.FunctionRevisionGroupVersionKind)),
 			},
 		},
 		Spec: appsv1.DeploymentSpec{
@@ -330,8 +333,13 @@ func buildFunctionDeployment(function *pkgmetav1alpha1.Function, revision v1.Pac
 	}
 	d.Spec.Template.Labels = templateLabels
 
+	// We want a headless service so that our gRPC client (i.e. the Crossplane
+	// FunctionComposer) can load balance across the endpoints.
+	// https://kubernetes.io/docs/concepts/services-networking/service/#headless-services
 	pkgName := revision.GetLabels()[v1.LabelParentPackage]
 	svc := getService(pkgName, namespace, revision.GetOwnerReferences(), d.Spec.Selector.MatchLabels)
+	svc.Spec.ClusterIP = corev1.ClusterIPNone
+
 	return s, d, svc, sec
 }
 
diff --git a/internal/controller/pkg/revision/hook.go b/internal/controller/pkg/revision/hook.go
index 649852028..d40475dca 100644
--- a/internal/controller/pkg/revision/hook.go
+++ b/internal/controller/pkg/revision/hook.go
@@ -31,9 +31,10 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
-	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	"github.com/crossplane/crossplane/internal/initializer"
 	"github.com/crossplane/crossplane/internal/xpkg"
 )
@@ -249,8 +250,8 @@ func NewFunctionHooks(client resource.ClientApplicator, namespace, serviceAccoun
 // Pre cleans up a packaged controller and service account if the revision is
 // inactive.
 func (h *FunctionHooks) Pre(ctx context.Context, pkg runtime.Object, pr v1.PackageRevision) error {
-	fo, _ := xpkg.TryConvert(pkg, &pkgmetav1alpha1.Function{})
-	pkgFunction, ok := fo.(*pkgmetav1alpha1.Function)
+	fo, _ := xpkg.TryConvert(pkg, &pkgmetav1beta1.Function{})
+	pkgFunction, ok := fo.(*pkgmetav1beta1.Function)
 	if !ok {
 		return errors.New(errNotFunction)
 	}
@@ -280,8 +281,8 @@ func (h *FunctionHooks) Pre(ctx context.Context, pkg runtime.Object, pr v1.Packa
 // Post creates a packaged function deployment, service account, service and secrets if the revision is active.
 func (h *FunctionHooks) Post(ctx context.Context, pkg runtime.Object, pr v1.PackageRevision) error { //nolint:gocyclo // See below
 	// TODO(ezgidemirel): Can this be refactored for less complexity?
-	po, _ := xpkg.TryConvert(pkg, &pkgmetav1alpha1.Function{})
-	pkgFunction, ok := po.(*pkgmetav1alpha1.Function)
+	po, _ := xpkg.TryConvert(pkg, &pkgmetav1beta1.Function{})
+	pkgFunction, ok := po.(*pkgmetav1beta1.Function)
 	if !ok {
 		return errors.New(errNotFunction)
 	}
@@ -316,7 +317,7 @@ func (h *FunctionHooks) Post(ctx context.Context, pkg runtime.Object, pr v1.Pack
 		return errors.Wrap(err, errApplyFunctionService)
 	}
 
-	fRev := pr.(*v1alpha1.FunctionRevision)
+	fRev := pr.(*v1beta1.FunctionRevision)
 	fRev.Status.Endpoint = fmt.Sprintf(serviceEndpointFmt, svc.Name, svc.Namespace, servicePort)
 
 	pr.SetControllerReference(v1.ControllerReference{Name: d.GetName()})
diff --git a/internal/controller/pkg/revision/reconciler.go b/internal/controller/pkg/revision/reconciler.go
index f1c7f2af5..695d90eb1 100644
--- a/internal/controller/pkg/revision/reconciler.go
+++ b/internal/controller/pkg/revision/reconciler.go
@@ -43,7 +43,7 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
 
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
-	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
 	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
@@ -321,8 +321,8 @@ func SetupConfigurationRevision(mgr ctrl.Manager, o controller.Options) error {
 
 // SetupFunctionRevision adds a controller that reconciles FunctionRevisions.
 func SetupFunctionRevision(mgr ctrl.Manager, o controller.Options) error {
-	name := "packages/" + strings.ToLower(v1alpha1.FunctionRevisionGroupKind)
-	nr := func() v1.PackageRevision { return &v1alpha1.FunctionRevision{} }
+	name := "packages/" + strings.ToLower(v1beta1.FunctionRevisionGroupKind)
+	nr := func() v1.PackageRevision { return &v1beta1.FunctionRevision{} }
 
 	clientset, err := kubernetes.NewForConfig(mgr.GetConfig())
 	if err != nil {
@@ -360,7 +360,7 @@ func SetupFunctionRevision(mgr ctrl.Manager, o controller.Options) error {
 
 	return ctrl.NewControllerManagedBy(mgr).
 		Named(name).
-		For(&v1alpha1.FunctionRevision{}).
+		For(&v1beta1.FunctionRevision{}).
 		Owns(&appsv1.Deployment{}).
 		Watches(&v1alpha1.ControllerConfig{}, &EnqueueRequestForReferencingFunctionRevisions{
 			client: mgr.GetClient(),
@@ -586,7 +586,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
 		return reconcile.Result{}, err
 	}
 
-	pkgMeta, _ := xpkg.TryConvert(pkg.GetMeta()[0], &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1alpha1.Function{})
+	pkgMeta, _ := xpkg.TryConvert(pkg.GetMeta()[0], &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1beta1.Function{})
 
 	pmo := pkgMeta.(metav1.Object)
 	meta.AddLabels(pr, pmo.GetLabels())
diff --git a/internal/controller/pkg/revision/watch.go b/internal/controller/pkg/revision/watch.go
index 5a96b57f2..4ba57a748 100644
--- a/internal/controller/pkg/revision/watch.go
+++ b/internal/controller/pkg/revision/watch.go
@@ -28,6 +28,7 @@ import (
 
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 )
 
 type adder interface {
@@ -124,7 +125,7 @@ func (e *EnqueueRequestForReferencingFunctionRevisions) add(ctx context.Context,
 		return
 	}
 
-	l := &v1alpha1.FunctionRevisionList{}
+	l := &v1beta1.FunctionRevisionList{}
 	if err := e.client.List(ctx, l); err != nil {
 		// TODO(hasheddan): Handle this error?
 		return
diff --git a/internal/features/features.go b/internal/features/features.go
index c0b079b39..e6f542232 100644
--- a/internal/features/features.go
+++ b/internal/features/features.go
@@ -19,22 +19,18 @@ package features
 
 import "github.com/crossplane/crossplane-runtime/pkg/feature"
 
-// Feature flags.
+// Alpha Feature flags.
 const (
 	// EnableAlphaEnvironmentConfigs enables alpha support for composition
 	// environments. See the below design for more details.
 	// https://github.com/crossplane/crossplane/blob/c4bcbe/design/one-pager-composition-environment.md
 	EnableAlphaEnvironmentConfigs feature.Flag = "EnableAlphaEnvironmentConfigs"
+
 	// EnableAlphaExternalSecretStores enables alpha support for
 	// External Secret Stores. See the below design for more details.
 	// https://github.com/crossplane/crossplane/blob/390ddd/design/design-doc-external-secret-stores.md
 	EnableAlphaExternalSecretStores feature.Flag = "EnableAlphaExternalSecretStores"
 
-	// EnableAlphaCompositionFunctions enables alpha support for composition
-	// functions. See the below design for more details.
-	// https://github.com/crossplane/crossplane/blob/9ee7a2/design/design-doc-composition-functions.md
-	EnableAlphaCompositionFunctions feature.Flag = "EnableAlphaCompositionFunctions"
-
 	// EnableAlphaCompositionWebhookSchemaValidation enables alpha support for
 	// composition webhook schema validation. See the below design for more
 	// details.
@@ -46,3 +42,11 @@ const (
 	// https://github.com/crossplane/crossplane/blob/19ea23e7c1fc16b20581755540f9f45afdf89338/design/one-pager-generic-usage-type.md
 	EnableAlphaUsages feature.Flag = "EnableAlphaUsages"
 )
+
+// Beta Feature Flags
+const (
+	// EnableBetaCompositionFunctions enables alpha support for composition
+	// functions. See the below design for more details.
+	// https://github.com/crossplane/crossplane/blob/863ff6/design/design-doc-composition-functions.md
+	EnableBetaCompositionFunctions feature.Flag = "EnableBetaCompositionFunctions"
+)
diff --git a/internal/xcrd/schemas.go b/internal/xcrd/schemas.go
index 29cd074ba..26d3dc504 100644
--- a/internal/xcrd/schemas.go
+++ b/internal/xcrd/schemas.go
@@ -16,7 +16,10 @@ limitations under the License.
 
 package xcrd
 
-import extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+import (
+	extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/utils/pointer"
+)
 
 // Label keys.
 const (
@@ -153,6 +156,8 @@ func CompositeResourceSpecProps() map[string]extv1.JSONSchemaProps {
 					Required: []string{"apiVersion", "kind"},
 				},
 			},
+			// Controllers should replace the entire resourceRefs array.
+			XListType: pointer.String("atomic"),
 		},
 		"publishConnectionDetailsTo": {
 			Type:     "object",
diff --git a/internal/xfn/function_runner.go b/internal/xfn/function_runner.go
new file mode 100644
index 000000000..bd8f8538c
--- /dev/null
+++ b/internal/xfn/function_runner.go
@@ -0,0 +1,284 @@
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+*/
+
+// Package xfn contains functionality for running Composition Functions.
+package xfn
+
+import (
+	"context"
+	"crypto/tls"
+	"sync"
+	"time"
+
+	"github.com/pkg/errors"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/credentials/insecure"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/logging"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1beta1"
+	pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
+	pkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
+)
+
+// Error strings
+const (
+	errListFunctionRevisions = "cannot list FunctionRevisions"
+	errNoActiveRevisions     = "cannot find an active FunctionRevision (a FunctionRevision with spec.desiredState: Active)"
+	errListFunctions         = "cannot List Functions to determine which gRPC client connections to garbage collect."
+
+	errFmtGetClientConn = "cannot get gRPC client connection for Function %q"
+	errFmtRunFunction   = "cannot run Function %q"
+	errFmtEmptyEndpoint = "cannot determine gRPC target: active FunctionRevision %q has an empty status.endpoint"
+	errFmtDialFunction  = "cannot gRPC dial target %q from status.endpoint of active FunctionRevision %q"
+)
+
+// TODO(negz): Should any of these be configurable?
+const (
+	// This configures a gRPC client to use round robin load balancing. This
+	// means that if the Function Deployment has more than one Pod, and the
+	// Function Service is headless, requests will be spread across each Pod.
+	// See https://github.com/grpc/grpc/blob/v1.58.0/doc/load-balancing.md#load-balancing-policies
+	lbRoundRobin = `{"loadBalancingConfig":[{"round_robin":{}}]}`
+
+	dialFunctionTimeout = 10 * time.Second
+	runFunctionTimeout  = 10 * time.Second
+)
+
+// A PackagedFunctionRunner runs a Function by making a gRPC call to a Function
+// package's runtime. It creates a gRPC client connection for each Function. The
+// Function's endpoint is determined by reading the status.endpoint of the
+// active FunctionRevision. You must call GarbageCollectClientConnections in
+// order to ensure connections are properly closed.
+type PackagedFunctionRunner struct {
+	client client.Reader
+	creds  credentials.TransportCredentials
+
+	connsMx sync.RWMutex
+	conns   map[string]*grpc.ClientConn
+
+	log logging.Logger
+}
+
+// A PackagedFunctionRunnerOption configures a PackagedFunctionRunner.
+type PackagedFunctionRunnerOption func(r *PackagedFunctionRunner)
+
+// WithLogger configures the logger the PackageFunctionRunner should use.
+func WithLogger(l logging.Logger) PackagedFunctionRunnerOption {
+	return func(r *PackagedFunctionRunner) {
+		r.log = l
+	}
+}
+
+// WithTLSConfig configures the client TLS the PackageFunctionRunner should use.
+func WithTLSConfig(cfg *tls.Config) PackagedFunctionRunnerOption {
+	return func(r *PackagedFunctionRunner) {
+		r.creds = credentials.NewTLS(cfg)
+	}
+}
+
+// NewPackagedFunctionRunner returns a FunctionRunner that runs a Function by
+// making a gRPC call to a Function package's runtime.
+func NewPackagedFunctionRunner(c client.Reader, o ...PackagedFunctionRunnerOption) *PackagedFunctionRunner {
+	r := &PackagedFunctionRunner{
+		client: c,
+		creds:  insecure.NewCredentials(),
+		conns:  make(map[string]*grpc.ClientConn),
+		log:    logging.NewNopLogger(),
+	}
+
+	for _, fn := range o {
+		fn(r)
+	}
+
+	return r
+}
+
+// RunFunction sends the supplied RunFunctionRequest to the named Function. The
+// function is expected to be an installed Function.pkg.crossplane.io package.
+func (r *PackagedFunctionRunner) RunFunction(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (*v1beta1.RunFunctionResponse, error) {
+	conn, err := r.getClientConn(ctx, name)
+	if err != nil {
+		return nil, errors.Wrapf(err, errFmtGetClientConn, name)
+	}
+
+	// This context is used for actually making the request.
+	ctx, cancel := context.WithTimeout(ctx, runFunctionTimeout)
+	defer cancel()
+
+	rsp, err := v1beta1.NewFunctionRunnerServiceClient(conn).RunFunction(ctx, req)
+	return rsp, errors.Wrapf(err, errFmtRunFunction, name)
+}
+
+// In most cases our gRPC target will be a Kubernetes Service. The package
+// manager creates this service for each active FunctionRevision, but the
+// Service is aligned with the Function. It's name is derived from the Function
+// (not the FunctionRevision). This means the target won't change just because a
+// new FunctionRevision was created.
+//
+// However, once the runtime config design is implemented it's possible that
+// something other than the package manager will reconcile FunctionRevisions.
+// There's no guarantee it will create a Service, or that the endpoint will
+// remain stable across FunctionRevisions.
+//
+// https://github.com/crossplane/crossplane/blob/226b81f/design/one-pager-package-runtime-config.md
+//
+// With this in mind, we attempt to:
+//
+// * Create a connection the first time someone runs a Function.
+// * Cache it so we don't pay the setup cost every time the Function is called.
+// * Verify that it has the correct target every time the Function is called.
+//
+// In the happy path, where a client already exists, this means we'll pay the
+// cost of listing and iterating over FunctionRevisions from cache. The default
+// RevisionHistoryLimit is 1, so for most Functions we'd expect there to be two
+// revisions in the cache (one active, and one previously active).
+func (r *PackagedFunctionRunner) getClientConn(ctx context.Context, name string) (*grpc.ClientConn, error) {
+	log := r.log.WithValues("function", name)
+
+	l := &pkgv1beta1.FunctionRevisionList{}
+	if err := r.client.List(ctx, l, client.MatchingLabels{pkgv1.LabelParentPackage: name}); err != nil {
+		return nil, errors.Wrapf(err, errListFunctionRevisions)
+	}
+
+	var active *pkgv1beta1.FunctionRevision
+	for i := range l.Items {
+		if l.Items[i].GetDesiredState() == pkgv1.PackageRevisionActive {
+			active = &l.Items[i]
+			break
+		}
+	}
+	if active == nil {
+		return nil, errors.New(errNoActiveRevisions)
+	}
+
+	if active.Status.Endpoint == "" {
+		return nil, errors.Errorf(errFmtEmptyEndpoint, active.GetName())
+	}
+
+	r.connsMx.RLock()
+	conn, ok := r.conns[name]
+	r.connsMx.RUnlock()
+
+	if ok {
+		// We have a connection for the up-to-date endpoint. Return it.
+		if conn.Target() == active.Status.Endpoint {
+			return conn, nil
+		}
+
+		// This connection is to an old endpoint. We need to close it and create
+		// a new connection. Close only returns an error is if the connection is
+		// already closed or in the process of closing.
+		log.Debug("Closing gRPC client connection with stale target", "old-target", conn.Target(), "new-target", active.Status.Endpoint)
+		_ = conn.Close()
+	}
+
+	// This context is only used for setting up the connection.
+	ctx, cancel := context.WithTimeout(ctx, dialFunctionTimeout)
+	defer cancel()
+
+	conn, err := grpc.DialContext(ctx, active.Status.Endpoint,
+		grpc.WithTransportCredentials(r.creds),
+		grpc.WithDefaultServiceConfig(lbRoundRobin))
+	if err != nil {
+		return nil, errors.Wrapf(err, errFmtDialFunction, active.Status.Endpoint, active.GetName())
+	}
+
+	r.connsMx.Lock()
+	r.conns[name] = conn
+	r.connsMx.Unlock()
+
+	log.Debug("Created new gRPC client connection", "target", active.Status.Endpoint)
+	return conn, nil
+}
+
+// GarbageCollectConnections runs every interval until the supplied context is
+// cancelled. It garbage collects gRPC client connections to Functions that are
+// no longer installed.
+func (r *PackagedFunctionRunner) GarbageCollectConnections(ctx context.Context, interval time.Duration) {
+	t := time.NewTicker(interval)
+	defer t.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			r.log.Debug("Stopping gRPC client connection garbage collector", "error", ctx.Err())
+			return
+		case <-t.C:
+			if _, err := r.GarbageCollectConnectionsNow(ctx); err != nil {
+				r.log.Info("Cannot garbage collect gRPC client connections", "error", err)
+			}
+		}
+	}
+}
+
+// GarbageCollectConnectionsNow immediately garbage collects any gRPC client
+// connections to Functions that are no longer installed. It returns the number
+// of connections garbage collected.
+func (r *PackagedFunctionRunner) GarbageCollectConnectionsNow(ctx context.Context) (int, error) {
+	// We try to take the write lock for as little time as possible,
+	// because while we have it RunFunction will block. In the happy
+	// path where no connections need garbage collecting we shouldn't
+	// take it at all.
+
+	r.connsMx.RLock()
+	connections := make([]string, 0, len(r.conns))
+	for name := range r.conns {
+		connections = append(connections, name)
+	}
+	r.connsMx.RUnlock()
+
+	// No need to list Functions if there's no work to do.
+	if len(connections) == 0 {
+		return 0, nil
+	}
+
+	l := &pkgv1beta1.FunctionList{}
+	if err := r.client.List(ctx, l); err != nil {
+		return 0, errors.Wrap(err, errListFunctions)
+	}
+
+	functionExists := map[string]bool{}
+	for _, f := range l.Items {
+		functionExists[f.GetName()] = true
+	}
+
+	// Build a list of connections to garbage collect.
+	gc := make([]string, 0)
+	for _, name := range connections {
+		if !functionExists[name] {
+			gc = append(gc, name)
+		}
+	}
+
+	// No need to take a write lock if there's no work to do.
+	if len(gc) == 0 {
+		return 0, nil
+	}
+
+	r.log.Debug("Closing gRPC client connections for Functions that are no longer installed", "functions", gc)
+	r.connsMx.Lock()
+	for _, name := range gc {
+		// Close only returns an error is if the connection is already
+		// closed or in the process of closing.
+		_ = r.conns[name].Close()
+		delete(r.conns, name)
+	}
+	r.connsMx.Unlock()
+
+	return len(gc), nil
+}
diff --git a/internal/xpkg/lint.go b/internal/xpkg/lint.go
index 6df2adb89..cac47384a 100644
--- a/internal/xpkg/lint.go
+++ b/internal/xpkg/lint.go
@@ -28,7 +28,7 @@ import (
 
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
-	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 	"github.com/crossplane/crossplane/internal/version"
 )
 
@@ -98,7 +98,7 @@ func IsConfiguration(o runtime.Object) error {
 
 // IsFunction checks that an object is a Function meta type.
 func IsFunction(o runtime.Object) error {
-	if _, ok := o.(*pkgmetav1alpha1.Function); !ok {
+	if _, ok := o.(*pkgmetav1beta1.Function); !ok {
 		return errors.New(errNotMetaFunction)
 	}
 	return nil
@@ -108,7 +108,7 @@ func IsFunction(o runtime.Object) error {
 // compatible with the package constraints.
 func PackageCrossplaneCompatible(v version.Operations) parser.ObjectLinterFn {
 	return func(o runtime.Object) error {
-		p, ok := TryConvertToPkg(o, &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1alpha1.Function{})
+		p, ok := TryConvertToPkg(o, &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1beta1.Function{})
 		if !ok {
 			return errors.New(errNotMeta)
 		}
@@ -129,7 +129,7 @@ func PackageCrossplaneCompatible(v version.Operations) parser.ObjectLinterFn {
 
 // PackageValidSemver checks that the package uses valid semver ranges.
 func PackageValidSemver(o runtime.Object) error {
-	p, ok := TryConvertToPkg(o, &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1alpha1.Function{})
+	p, ok := TryConvertToPkg(o, &pkgmetav1.Provider{}, &pkgmetav1.Configuration{}, &pkgmetav1beta1.Function{})
 	if !ok {
 		return errors.New(errNotMeta)
 	}
diff --git a/internal/xpkg/scheme.go b/internal/xpkg/scheme.go
index 5cf3a935c..c046b45d2 100644
--- a/internal/xpkg/scheme.go
+++ b/internal/xpkg/scheme.go
@@ -26,6 +26,7 @@ import (
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
 	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 )
 
 // BuildMetaScheme builds the default scheme used for identifying metadata in a
@@ -35,6 +36,9 @@ func BuildMetaScheme() (*runtime.Scheme, error) {
 	if err := pkgmetav1alpha1.SchemeBuilder.AddToScheme(metaScheme); err != nil {
 		return nil, err
 	}
+	if err := pkgmetav1beta1.SchemeBuilder.AddToScheme(metaScheme); err != nil {
+		return nil, err
+	}
 	if err := pkgmetav1.SchemeBuilder.AddToScheme(metaScheme); err != nil {
 		return nil, err
 	}



Test patch:
diff --git a/apis/apiextensions/v1/composition_validation_test.go b/apis/apiextensions/v1/composition_validation_test.go
index e7550e730..9be4db1b1 100644
--- a/apis/apiextensions/v1/composition_validation_test.go
+++ b/apis/apiextensions/v1/composition_validation_test.go
@@ -33,6 +33,112 @@ func sortFieldErrors() cmp.Option {
 	})
 }
 
+func TestCompositionValidateMode(t *testing.T) {
+	type args struct {
+		spec CompositionSpec
+	}
+	type want struct {
+		output field.ErrorList
+	}
+
+	resources := CompositionModeResources
+	pipeline := CompositionModePipeline
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"ValidResources": {
+			reason: "A Resources mode Composition with an array of resources is valid",
+			args: args{
+				spec: CompositionSpec{
+					Mode: &resources,
+					Resources: []ComposedTemplate{
+						{Name: pointer.String("cool-template")},
+					},
+				},
+			},
+			want: want{
+				output: nil,
+			},
+		},
+		"ValidImplicitResources": {
+			reason: "A Composition with no explicit mode is assumed to be a Resources mode Composition, which with an array of resources is valid",
+			args: args{
+				spec: CompositionSpec{
+					// This Composition uses Resources mode implicitly.
+					Resources: []ComposedTemplate{
+						{Name: pointer.String("cool-template")},
+					},
+				},
+			},
+			want: want{
+				output: nil,
+			},
+		},
+		"InvalidResources": {
+			reason: "A Resources mode Composition without an array of resources is invalid",
+			args: args{
+				spec: CompositionSpec{
+					Mode: &resources,
+				},
+			},
+			want: want{
+				output: field.ErrorList{field.Required(field.NewPath("spec", "resources"), "this test ignores this field")},
+			},
+		},
+		"InvalidImplicitResources": {
+			reason: "A Composition with no explicit mode is assumed to be a Resources mode Composition, which is invalid without an array of resources",
+			args: args{
+				spec: CompositionSpec{},
+			},
+			want: want{
+				output: field.ErrorList{field.Required(field.NewPath("spec", "resources"), "this test ignores this field")},
+			},
+		},
+		"ValidPipeline": {
+			reason: "A Pipeline mode Composition with an array of pipeline steps is valid",
+			args: args{
+				spec: CompositionSpec{
+					Mode: &pipeline,
+					Pipeline: []PipelineStep{
+						{
+							Step: "razor",
+						},
+					},
+				},
+			},
+			want: want{
+				output: nil,
+			},
+		},
+		"InvalidPipeline": {
+			reason: "A Pipeline mode Composition without an array of pipeline steps is invalid",
+			args: args{
+				spec: CompositionSpec{
+					Mode: &pipeline,
+				},
+			},
+			want: want{
+				output: field.ErrorList{field.Required(field.NewPath("spec", "pipeline"), "this test ignores this field")},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			c := &Composition{
+				Spec: tc.args.spec,
+			}
+			gotErrs := c.validateMode()
+			if diff := cmp.Diff(tc.want.output, gotErrs, sortFieldErrors(), cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
+				t.Errorf("%s\nvalidateResourceNames(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
 func TestCompositionValidateResourceName(t *testing.T) {
 	type args struct {
 		spec CompositionSpec
@@ -119,9 +225,9 @@ func TestCompositionValidateResourceName(t *testing.T) {
 						{Name: pointer.String("foo")},
 						{Name: pointer.String("bar")},
 					},
-					Functions: []Function{
+					Pipeline: []PipelineStep{
 						{
-							Name: "baz",
+							Step: "baz",
 						},
 					},
 				},
@@ -134,9 +240,9 @@ func TestCompositionValidateResourceName(t *testing.T) {
 					Resources: []ComposedTemplate{
 						{},
 					},
-					Functions: []Function{
+					Pipeline: []PipelineStep{
 						{
-							Name: "foo",
+							Step: "foo",
 						},
 					},
 				},
@@ -341,7 +447,7 @@ func TestCompositionValidatePatchSets(t *testing.T) {
 	}
 }
 
-func TestCompositionValidateFunctions(t *testing.T) {
+func TestCompositionValidatePipeline(t *testing.T) {
 	type args struct {
 		comp *Composition
 	}
@@ -354,7 +460,7 @@ func TestCompositionValidateFunctions(t *testing.T) {
 		want   want
 	}{
 		"ValidNoFunctions": {
-			reason: "no functions should be valid",
+			reason: "no steps should be valid",
 			args: args{
 				comp: &Composition{
 					Spec: CompositionSpec{},
@@ -362,80 +468,33 @@ func TestCompositionValidateFunctions(t *testing.T) {
 			},
 		},
 		"ValidFunctions": {
-			reason: "functions with valid configuration should be valid",
-			args: args{
-				comp: &Composition{
-					Spec: CompositionSpec{
-						Functions: []Function{
-							{
-								Name: "foo",
-								Type: FunctionTypeContainer,
-								Container: &ContainerFunction{
-									Image: "foo",
-								},
-							},
-							{
-								Name: "bar",
-								Type: FunctionTypeContainer,
-								Container: &ContainerFunction{
-									Image: "bar",
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-		"InvalidDuplicateFuctionNames": {
-			reason: "Invalid functions with duplicate names",
+			reason: "steps with valid configuration should be valid",
 			args: args{
 				comp: &Composition{
 					Spec: CompositionSpec{
-						Functions: []Function{
+						Pipeline: []PipelineStep{
 							{
-								Name: "foo",
-								Type: FunctionTypeContainer,
-								Container: &ContainerFunction{
-									Image: "foo",
-								},
+								Step: "foo",
 							},
 							{
-								Name: "foo",
-								Type: FunctionTypeContainer,
-								Container: &ContainerFunction{
-									Image: "bar",
-								},
+								Step: "bar",
 							},
 						},
 					},
 				},
 			},
-			want: want{
-				output: field.ErrorList{
-					{
-						Type:     field.ErrorTypeDuplicate,
-						Field:    "spec.functions[1].name",
-						BadValue: "foo",
-					},
-				},
-			},
 		},
-		"InvalidDuplicateFuctionNamesAndMissingContainer": {
-			reason: "functions with duplicate names and missing container should return both validation errors",
+		"InvalidDuplicateStepNames": {
+			reason: "Invalid steps with duplicate names",
 			args: args{
 				comp: &Composition{
 					Spec: CompositionSpec{
-						Functions: []Function{
+						Pipeline: []PipelineStep{
 							{
-								Name: "foo",
-								Type: FunctionTypeContainer,
-								Container: &ContainerFunction{
-									Image: "foo",
-								},
+								Step: "foo",
 							},
 							{
-								Name: "foo",
-								Type: FunctionTypeContainer,
+								Step: "foo",
 							},
 						},
 					},
@@ -445,23 +504,18 @@ func TestCompositionValidateFunctions(t *testing.T) {
 				output: field.ErrorList{
 					{
 						Type:     field.ErrorTypeDuplicate,
-						Field:    "spec.functions[1].name",
+						Field:    "spec.pipeline[1].step",
 						BadValue: "foo",
 					},
-					{
-						Type:     field.ErrorTypeRequired,
-						Field:    "spec.functions[1].container",
-						BadValue: "",
-					},
 				},
 			},
 		},
 	}
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
-			gotErrs := tc.args.comp.validateFunctions()
+			gotErrs := tc.args.comp.validatePipeline()
 			if diff := cmp.Diff(tc.want.output, gotErrs, sortFieldErrors(), cmpopts.IgnoreFields(field.Error{}, "Detail", "BadValue")); diff != "" {
-				t.Errorf("%s\nvalidateFunctions(...): -want, +got:\n%s", tc.reason, diff)
+				t.Errorf("%s\nvalidatePipeline(...): -want, +got:\n%s", tc.reason, diff)
 			}
 		})
 	}
diff --git a/apis/pkg/meta/v1beta1/function_interfaces_test.go b/apis/pkg/meta/v1beta1/function_interfaces_test.go
new file mode 100644
index 000000000..9ddba9c7d
--- /dev/null
+++ b/apis/pkg/meta/v1beta1/function_interfaces_test.go
@@ -0,0 +1,23 @@
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import (
+	v1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
+)
+
+var _ v1.Pkg = &Function{}
diff --git a/apis/pkg/v1/interfaces_test.go b/apis/pkg/v1/interfaces_test.go
new file mode 100644
index 000000000..f6c2e3ae7
--- /dev/null
+++ b/apis/pkg/v1/interfaces_test.go
@@ -0,0 +1,26 @@
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1
+
+var _ Package = &Provider{}
+var _ Package = &Configuration{}
+
+var _ PackageRevision = &ProviderRevision{}
+var _ PackageRevision = &ConfigurationRevision{}
+
+var _ PackageRevisionList = &ProviderRevisionList{}
+var _ PackageRevisionList = &ConfigurationRevisionList{}
diff --git a/apis/pkg/v1beta1/function_interfaces_test.go b/apis/pkg/v1beta1/function_interfaces_test.go
new file mode 100644
index 000000000..783149ee4
--- /dev/null
+++ b/apis/pkg/v1beta1/function_interfaces_test.go
@@ -0,0 +1,23 @@
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import v1 "github.com/crossplane/crossplane/apis/pkg/v1"
+
+var _ v1.Package = &Function{}
+var _ v1.PackageRevision = &FunctionRevision{}
+var _ v1.PackageRevisionList = &FunctionRevisionList{}
diff --git a/internal/controller/apiextensions/composite/composition_fallback_test.go b/internal/controller/apiextensions/composite/composition_fallback_test.go
deleted file mode 100644
index 7ded4fcbb..000000000
--- a/internal/controller/apiextensions/composite/composition_fallback_test.go
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
-Copyright 2022 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package composite
-
-import (
-	"context"
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	corev1 "k8s.io/api/core/v1"
-	kerrors "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-
-	"github.com/crossplane/crossplane-runtime/pkg/errors"
-	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
-	"github.com/crossplane/crossplane-runtime/pkg/resource"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
-	"github.com/crossplane/crossplane-runtime/pkg/test"
-
-	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
-)
-
-type MockComposer struct {
-	res CompositionResult
-	err error
-}
-
-func (c *MockComposer) Compose(_ context.Context, _ resource.Composite, _ CompositionRequest) (CompositionResult, error) {
-	return c.res, c.err
-}
-
-func TestFallbackComposer(t *testing.T) {
-	errBoom := errors.New("boom")
-	conns := managed.ConnectionDetails{"a": []byte("b")}
-
-	type params struct {
-		preferred Composer
-		fallback  Composer
-		fn        TriggerFn
-	}
-	type args struct {
-		ctx context.Context
-		xr  resource.Composite
-		req CompositionRequest
-	}
-	type want struct {
-		res CompositionResult
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"SimplePreferred": {
-			reason: "We should call the preferred Composer if the TriggerFn returns false.",
-			params: params{
-				preferred: &MockComposer{res: CompositionResult{
-					ConnectionDetails: conns,
-				}},
-				fallback: &MockComposer{res: CompositionResult{}},
-				fn: func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
-					// Don't fall back.
-					return false, nil
-				},
-			},
-			want: want{
-				res: CompositionResult{
-					ConnectionDetails: conns,
-				},
-			},
-		},
-		"SimpleFallback": {
-			reason: "We should call the fallback Composer if the TriggerFn returns true.",
-			params: params{
-				preferred: &MockComposer{res: CompositionResult{}},
-				fallback: &MockComposer{res: CompositionResult{
-					ConnectionDetails: conns,
-				}},
-				fn: func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
-					// Fall back.
-					return true, nil
-				},
-			},
-			want: want{
-				res: CompositionResult{
-					ConnectionDetails: conns,
-				},
-			},
-		},
-		"TriggerFnError": {
-			reason: "We should return any error returned by the trigger function.",
-			params: params{
-				preferred: &MockComposer{res: CompositionResult{}},
-				fallback:  &MockComposer{res: CompositionResult{}},
-				fn: func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
-					return true, errBoom
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errTriggerFn),
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			c := NewFallBackComposer(tc.params.preferred, tc.params.fallback, tc.params.fn)
-			res, err := c.Compose(tc.args.ctx, tc.args.xr, tc.args.req)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			// We need to EquateErrors here for RenderErrors.
-			if diff := cmp.Diff(tc.want.res, res, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nComposer(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestFallBackForAnonymousTemplates(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type params struct {
-		c client.Reader
-	}
-	type args struct {
-		ctx context.Context
-		xr  resource.Composite
-		req CompositionRequest
-	}
-	type want struct {
-		fallback bool
-		err      error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"HasAnonymousTemplate": {
-			reason: "We should fallback if the supplied Composition has an anonymous template.",
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Resources: []v1.ComposedTemplate{
-								{}, // A resource without a name.
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				fallback: true,
-			},
-		},
-		"ReferencesResourceCreatedWithAnonymousTemplate": {
-			reason: "We should fallback if the supplied XR references a composed resource created using an anonymous template.",
-			params: params{
-				c: &test.MockClient{
-					// We'll return whatever object the supplier passes in
-					// unmodified, and therefore the object will be missing the
-					// annotation that indicates the composition resource name
-					// this resource was created from.
-					MockGet: test.NewMockGetFn(nil),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{{Name: "cool-resource"}},
-					},
-				},
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{},
-				},
-			},
-			want: want{
-				fallback: true,
-			},
-		},
-		"GetResourceError": {
-			reason: "We should return an error if we can't get a referenced composed resource.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(errBoom),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{{Name: "cool-resource"}},
-					},
-				},
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errGetComposed),
-			},
-		},
-		"NoResourceTemplatesOrExistingComposedResources": {
-			reason: "If there are no resource templates or composed resources (e.g. a new Composition Functions based XR) we should not fallback.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-						return kerrors.NewNotFound(schema.GroupResource{}, "")
-					}),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{},
-					},
-				},
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Functions: []v1.Function{{
-								Name: "cool-fn",
-								Type: v1.FunctionTypeContainer,
-								Container: &v1.ContainerFunction{
-									Image: "cool-img:latest",
-								},
-							}},
-						},
-					},
-				},
-			},
-			want: want{
-				fallback: false,
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			fn := FallBackForAnonymousTemplates(tc.params.c)
-			fallback, err := fn(tc.args.ctx, tc.args.xr, tc.args.req)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nFallBackForAnonymousTemplates(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.fallback, fallback); diff != "" {
-				t.Errorf("\n%s\nFallBackForAnonymousTemplates(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
diff --git a/internal/controller/apiextensions/composite/composition_functions_test.go b/internal/controller/apiextensions/composite/composition_functions_test.go
new file mode 100644
index 000000000..52a837a74
--- /dev/null
+++ b/internal/controller/apiextensions/composite/composition_functions_test.go
@@ -0,0 +1,1204 @@
+/*
+Copyright 2022 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package composite
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/testing/protocmp"
+	"google.golang.org/protobuf/types/known/structpb"
+	corev1 "k8s.io/api/core/v1"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/pointer"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/event"
+	"github.com/crossplane/crossplane-runtime/pkg/meta"
+	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
+	"github.com/crossplane/crossplane-runtime/pkg/resource"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1beta1"
+	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
+	"github.com/crossplane/crossplane/internal/xcrd"
+)
+
+func TestFunctionCompose(t *testing.T) {
+	errBoom := errors.New("boom")
+
+	errProtoSyntax := protojson.Unmarshal([]byte("hi"), &structpb.Struct{})
+
+	type params struct {
+		kube client.Client
+		r    FunctionRunner
+		o    []FunctionComposerOption
+	}
+	type args struct {
+		ctx context.Context
+		xr  resource.Composite
+		req CompositionRequest
+	}
+	type want struct {
+		res CompositionResult
+		err error
+	}
+
+	cases := map[string]struct {
+		reason string
+		params params
+		args   args
+		want   want
+	}{
+		"FetchConnectionError": {
+			reason: "We should return any error encountered while fetching the XR's connection details.",
+			params: params{
+				o: []FunctionComposerOption{
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return ComposedResourceStates{}, nil
+					})),
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, errBoom
+					})),
+				},
+			},
+			args: args{
+				xr:  composite.New(),
+				req: CompositionRequest{Revision: &v1.CompositionRevision{}},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errFetchXRConnectionDetails),
+			},
+		},
+		"GetComposedResourcesError": {
+			reason: "We should return any error encountered while getting the XR's existing composed resources.",
+			params: params{
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, errBoom
+					})),
+				},
+			},
+			args: args{
+				xr:  composite.New(),
+				req: CompositionRequest{Revision: &v1.CompositionRevision{}},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errGetExistingCDs),
+			},
+		},
+		"UnmarshalFunctionInputError": {
+			reason: "We should return any error encountered while unmarshalling a Composition Function input",
+			params: params{
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+				},
+			},
+			args: args{
+				xr: composite.New(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+									Input:       &runtime.RawExtension{Raw: []byte("hi")}, // This is invalid - it must be a JSON object.
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrapf(errProtoSyntax, errFmtUnmarshalPipelineStepInput, "run-cool-function"),
+			},
+		},
+		"RunFunctionError": {
+			reason: "We should return any error encountered while running a Composition Function",
+			params: params{
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					return nil, errBoom
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+				},
+			},
+			args: args{
+				xr: composite.New(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrapf(errBoom, errFmtRunPipelineStep, "run-cool-function"),
+			},
+		},
+		"FatalFunctionResultError": {
+			reason: "We should return any fatal function results as an error",
+			params: params{
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					r := &v1beta1.Result{
+						Severity: v1beta1.Severity_SEVERITY_FATAL,
+						Message:  "oh no",
+					}
+					return &v1beta1.RunFunctionResponse{Results: []*v1beta1.Result{r}}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+				},
+			},
+			args: args{
+				xr: composite.New(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Errorf(errFmtFatalResult, "run-cool-function", "oh no"),
+			},
+		},
+		"RenderComposedResourceMetadataError": {
+			reason: "We should return any error we encounter when rendering composed resource metadata",
+			params: params{
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					d := &v1beta1.State{
+						Resources: map[string]*v1beta1.Resource{
+							"cool-resource": {
+								Resource: MustStruct(map[string]any{
+									"apiVersion": "test.crossplane.io/v1",
+									"kind":       "CoolComposed",
+								}),
+							},
+						},
+					}
+					return &v1beta1.RunFunctionResponse{Desired: d}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+				},
+			},
+			args: args{
+				// Missing labels required by RenderComposedResourceMetadata.
+				xr: composite.New(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrapf(RenderComposedResourceMetadata(nil, composite.New(), ""), errFmtRenderMetadata, "cool-resource"),
+			},
+		},
+		"DryRunCreateComposedResourceError": {
+			reason: "We should return any error we encounter when dry-run creating a composed resource",
+			params: params{
+				kube: &test.MockClient{
+					MockCreate: test.NewMockCreateFn(errBoom),
+				},
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					d := &v1beta1.State{
+						Resources: map[string]*v1beta1.Resource{
+							"cool-resource": {
+								Resource: MustStruct(map[string]any{
+									"apiVersion": "test.crossplane.io/v1",
+									"kind":       "CoolComposed",
+
+									// No name means we'll dry-run apply.
+								}),
+							},
+						},
+					}
+					return &v1beta1.RunFunctionResponse{Desired: d}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+				},
+			},
+			args: args{
+				xr: WithParentLabel(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrapf(errBoom, errFmtDryRunCreateCD, "cool-resource"),
+			},
+		},
+		"GarbageCollectComposedResourcesError": {
+			reason: "We should return any error we encounter when garbage collecting composed resources",
+			params: params{
+				kube: &test.MockClient{
+					MockPatch: test.NewMockPatchFn(nil),
+				},
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					return &v1beta1.RunFunctionResponse{}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error {
+						return errBoom
+					})),
+				},
+			},
+			args: args{
+				xr: WithParentLabel(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errGarbageCollectCDs),
+			},
+		},
+		"ApplyXRResourceReferencesError": {
+			reason: "We should return any error we encounter when applying the composite resource's resource references",
+			params: params{
+				kube: &test.MockClient{
+					MockPatch: test.NewMockPatchFn(nil, func(obj client.Object) error {
+						// We only want to return an error for the XR.
+						u := obj.(*kunstructured.Unstructured)
+						if u.GetKind() == "CoolComposed" {
+							return nil
+						}
+						return errBoom
+					}),
+				},
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					return &v1beta1.RunFunctionResponse{}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error {
+						return nil
+					})),
+				},
+			},
+			args: args{
+				xr: composite.New(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errApplyXRRefs),
+			},
+		},
+		"ApplyXRStatusError": {
+			reason: "We should return any error we encounter when applying the composite resource status",
+			params: params{
+				kube: &test.MockClient{
+					MockPatch:       test.NewMockPatchFn(nil),
+					MockStatusPatch: test.NewMockSubResourcePatchFn(errBoom),
+				},
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					d := &v1beta1.State{
+						Composite: &v1beta1.Resource{
+							Resource: MustStruct(map[string]any{
+								"status": map[string]any{
+									"widgets": 42,
+								},
+							})},
+					}
+					return &v1beta1.RunFunctionResponse{Desired: d}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error {
+						return nil
+					})),
+				},
+			},
+			args: args{
+				xr: composite.New(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errApplyXRStatus),
+			},
+		},
+		"ApplyComposedResourceError": {
+			reason: "We should return any error we encounter when applying a composed resource",
+			params: params{
+				kube: &test.MockClient{
+					MockCreate: test.NewMockCreateFn(nil),
+					MockPatch: test.NewMockPatchFn(nil, func(obj client.Object) error {
+						// We only want to return an error if we're patching a
+						// composed resource.
+						u := obj.(*kunstructured.Unstructured)
+						if u.GetKind() == "UncoolComposed" {
+							return errBoom
+						}
+						return nil
+					}),
+					MockStatusPatch: test.NewMockSubResourcePatchFn(nil),
+				},
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (rsp *v1beta1.RunFunctionResponse, err error) {
+					d := &v1beta1.State{
+						Resources: map[string]*v1beta1.Resource{
+							"uncool-resource": {
+								Resource: MustStruct(map[string]any{
+									"apiVersion": "test.crossplane.io/v1",
+									"kind":       "UncoolComposed",
+								}),
+							},
+						},
+					}
+					return &v1beta1.RunFunctionResponse{Desired: d}, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						return nil, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error {
+						return nil
+					})),
+				},
+			},
+			args: args{
+				xr: WithParentLabel(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrapf(errBoom, errFmtApplyCD, "uncool-resource"),
+			},
+		},
+		"Successful": {
+			reason: "We should return a valid CompositionResult when a 'pure Function' (i.e. patch-and-transform-less) reconcile succeeds",
+			params: params{
+				kube: &test.MockClient{
+					MockCreate:      test.NewMockCreateFn(nil),
+					MockPatch:       test.NewMockPatchFn(nil),
+					MockStatusPatch: test.NewMockSubResourcePatchFn(nil),
+				},
+				r: FunctionRunnerFn(func(ctx context.Context, name string, req *v1beta1.RunFunctionRequest) (*v1beta1.RunFunctionResponse, error) {
+					rsp := &v1beta1.RunFunctionResponse{
+						Desired: &v1beta1.State{
+							Composite: &v1beta1.Resource{
+								Resource: MustStruct(map[string]any{
+									"status": map[string]any{
+										"widgets": 42,
+									},
+								}),
+								ConnectionDetails: map[string][]byte{"from": []byte("function-pipeline")},
+							},
+							Resources: map[string]*v1beta1.Resource{
+								"observed-resource-a": {
+									Resource: MustStruct(map[string]any{
+										"apiVersion": "test.crossplane.io/v1",
+										"kind":       "CoolComposed",
+									}),
+									Ready: v1beta1.Ready_READY_TRUE,
+								},
+								"desired-resource-a": {
+									Resource: MustStruct(map[string]any{
+										"apiVersion": "test.crossplane.io/v1",
+										"kind":       "CoolComposed",
+									}),
+								},
+							},
+						},
+						Results: []*v1beta1.Result{
+							{
+								Severity: v1beta1.Severity_SEVERITY_NORMAL,
+								Message:  "A normal result",
+							},
+							{
+								Severity: v1beta1.Severity_SEVERITY_WARNING,
+								Message:  "A warning result",
+							},
+							{
+								Severity: v1beta1.Severity_SEVERITY_UNSPECIFIED,
+								Message:  "A result of unspecified severity",
+							},
+						},
+					}
+					return rsp, nil
+				}),
+				o: []FunctionComposerOption{
+					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
+						// We only try to extract connection details for
+						// observed resources.
+						r := ComposedResourceStates{
+							"observed-resource-a": ComposedResourceState{
+								Resource: &fake.Composed{
+									ObjectMeta: metav1.ObjectMeta{Name: "observed-resource-a"},
+								},
+							},
+						}
+						return r, nil
+					})),
+					WithComposedResourceGarbageCollector(ComposedResourceGarbageCollectorFn(func(ctx context.Context, owner metav1.Object, observed, desired ComposedResourceStates) error {
+						return nil
+					})),
+				},
+			},
+			args: args{
+				xr: func() resource.Composite {
+					// Our XR needs a GVK to survive round-tripping through a
+					// protobuf struct (which involves using the Kubernetes-aware
+					// JSON unmarshaller that requires a GVK).
+					xr := composite.New(composite.WithGroupVersionKind(schema.GroupVersionKind{
+						Group:   "test.crossplane.io",
+						Version: "v1",
+						Kind:    "CoolComposite",
+					}))
+					xr.SetLabels(map[string]string{
+						xcrd.LabelKeyNamePrefixForComposed: "parent-xr",
+					})
+					return xr
+				}(),
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{
+						Spec: v1.CompositionRevisionSpec{
+							Pipeline: []v1.PipelineStep{
+								{
+									Step:        "run-cool-function",
+									FunctionRef: v1.FunctionReference{Name: "cool-function"},
+								},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				res: CompositionResult{
+					Composed: []ComposedResource{
+						{ResourceName: "desired-resource-a"},
+						{ResourceName: "observed-resource-a", Ready: true},
+					},
+					ConnectionDetails: managed.ConnectionDetails{
+						"from": []byte("function-pipeline"),
+					},
+					Events: []event.Event{
+						{
+							Type:    "Normal",
+							Reason:  "ComposeResources",
+							Message: "Pipeline step \"run-cool-function\": A normal result",
+						},
+						{
+							Type:    "Warning",
+							Reason:  "ComposeResources",
+							Message: "Pipeline step \"run-cool-function\": A warning result",
+						},
+						{
+							Type:    "Warning",
+							Reason:  "ComposeResources",
+							Message: "Pipeline step \"run-cool-function\" returned a result of unknown severity (assuming warning): A result of unspecified severity",
+						},
+					},
+				},
+				err: nil,
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+
+			c := NewFunctionComposer(tc.params.kube, tc.params.r, tc.params.o...)
+			res, err := c.Compose(tc.args.ctx, tc.args.xr, tc.args.req)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
+			}
+
+			// We iterate over a map to produce ComposedResources, so they're
+			// returned in random order.
+			if diff := cmp.Diff(tc.want.res, res, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(i, j ComposedResource) bool { return i.ResourceName < j.ResourceName })); diff != "" {
+				t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func MustStruct(v map[string]any) *structpb.Struct {
+	s, err := structpb.NewStruct(v)
+	if err != nil {
+		panic(err)
+	}
+	return s
+}
+
+func WithParentLabel() *composite.Unstructured {
+	xr := composite.New()
+	xr.SetLabels(map[string]string{xcrd.LabelKeyNamePrefixForComposed: "parent-xr"})
+	return xr
+}
+
+func TestGetComposedResources(t *testing.T) {
+	errBoom := errors.New("boom")
+	details := managed.ConnectionDetails{"a": []byte("b")}
+
+	type params struct {
+		c client.Reader
+		f managed.ConnectionDetailsFetcher
+	}
+
+	type args struct {
+		ctx context.Context
+		xr  resource.Composite
+	}
+
+	type want struct {
+		ors ComposedResourceStates
+		err error
+	}
+
+	cases := map[string]struct {
+		reason string
+		params params
+		args   args
+		want   want
+	}{
+		"UnnamedRef": {
+			reason: "We should skip any resource references without names.",
+			params: params{
+				c: &test.MockClient{
+					// We should continue past the unnamed reference and not hit
+					// this error.
+					MockGet: test.NewMockGetFn(errBoom),
+				},
+			},
+			args: args{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{
+								APIVersion: "example.org/v1",
+								Kind:       "Unnamed",
+							},
+						},
+					},
+				},
+			},
+		},
+		"ComposedResourceNotFound": {
+			reason: "We should skip any resources that are not found.",
+			params: params{
+				c: &test.MockClient{
+					MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
+				},
+			},
+			args: args{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{Name: "cool-resource"},
+						},
+					},
+				},
+			},
+		},
+		"GetComposedResourceError": {
+			reason: "We should return any error we encounter while getting a composed resource.",
+			params: params{
+				c: &test.MockClient{
+					MockGet: test.NewMockGetFn(errBoom),
+				},
+			},
+			args: args{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{Name: "cool-resource"},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errGetComposed),
+			},
+		},
+		"UncontrolledComposedResource": {
+			reason: "We should skip any composed resources our XR doesn't control.",
+			params: params{
+				c: &test.MockClient{
+					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
+						_ = meta.AddControllerReference(obj, metav1.OwnerReference{
+							UID:        types.UID("someone-else"),
+							Controller: pointer.Bool(true),
+						})
+
+						return nil
+					}),
+				},
+			},
+			args: args{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{Name: "cool-resource"},
+						},
+					},
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"AnonymousComposedResourceError": {
+			reason: "We should return an error if we encounter an (unsupported) anonymous composed resource.",
+			params: params{
+				c: &test.MockClient{
+					// We 'return' an empty resource with no annotations.
+					MockGet: test.NewMockGetFn(nil),
+				},
+			},
+			args: args{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{Name: "cool-resource"},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.New(errAnonymousCD),
+			},
+		},
+		"FetchConnectionDetailsError": {
+			reason: "We should return an error if we can't fetch composed resource connection details.",
+			params: params{
+				c: &test.MockClient{
+					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
+						obj.SetName("cool-resource-42")
+						SetCompositionResourceName(obj, "cool-resource")
+						return nil
+					}),
+				},
+				f: ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+					return nil, errBoom
+				}),
+			},
+			args: args{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{
+								Kind: "Broken",
+								Name: "cool-resource-42",
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrapf(errBoom, errFmtFetchCDConnectionDetails, "cool-resource", "Broken", "cool-resource-42"),
+			},
+		},
+		"Success": {
+			reason: "We should return any composed resources and their connection details.",
+			params: params{
+				c: &test.MockClient{
+					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
+						obj.SetName("cool-resource-42")
+						SetCompositionResourceName(obj, "cool-resource")
+						return nil
+					}),
+				},
+				f: ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+					return details, nil
+				}),
+			},
+			args: args{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{
+								APIVersion: "example.org/v1",
+								Kind:       "Composed",
+								Name:       "cool-resource-42",
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				ors: ComposedResourceStates{"cool-resource": ComposedResourceState{
+					ConnectionDetails: details,
+					Resource: func() resource.Composed {
+						cd := composed.New()
+						cd.SetAPIVersion("example.org/v1")
+						cd.SetKind("Composed")
+						cd.SetName("cool-resource-42")
+						SetCompositionResourceName(cd, "cool-resource")
+						return cd
+					}(),
+				},
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+
+			g := NewExistingComposedResourceObserver(tc.params.c, tc.params.f)
+			ors, err := g.ObserveComposedResources(tc.args.ctx, tc.args.xr)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nObserveComposedResources(...): -want, +got:\n%s", tc.reason, diff)
+			}
+
+			if diff := cmp.Diff(tc.want.ors, ors, cmpopts.EquateEmpty()); diff != "" {
+				t.Errorf("\n%s\nObserveComposedResources(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestAsState(t *testing.T) {
+	type args struct {
+		xr resource.Composite
+		xc managed.ConnectionDetails
+		rs ComposedResourceStates
+	}
+	type want struct {
+		d   *v1beta1.State
+		err error
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"Success": {
+			reason: "We should successfully build RunFunctionRequest State.",
+			args: args{
+				xr: composite.New(composite.WithGroupVersionKind(schema.GroupVersionKind{
+					Group:   "example.org",
+					Version: "v1",
+					Kind:    "Composite",
+				})),
+				xc: managed.ConnectionDetails{"a": []byte("b")},
+				rs: ComposedResourceStates{
+					"cool-resource": ComposedResourceState{
+						Resource: composed.New(composed.FromReference(corev1.ObjectReference{
+							APIVersion: "example.org/v2",
+							Kind:       "Composed",
+							Name:       "cool-resource-42",
+						})),
+					},
+				},
+			},
+			want: want{
+				d: &v1beta1.State{
+					Composite: &v1beta1.Resource{
+						Resource: &structpb.Struct{Fields: map[string]*structpb.Value{
+							"apiVersion": structpb.NewStringValue("example.org/v1"),
+							"kind":       structpb.NewStringValue("Composite"),
+						}},
+						ConnectionDetails: map[string][]byte{"a": []byte("b")},
+					},
+					Resources: map[string]*v1beta1.Resource{
+						"cool-resource": {
+							Resource: &structpb.Struct{Fields: map[string]*structpb.Value{
+								"apiVersion": structpb.NewStringValue("example.org/v2"),
+								"kind":       structpb.NewStringValue("Composed"),
+								"metadata": structpb.NewStructValue(&structpb.Struct{Fields: map[string]*structpb.Value{
+									"name": structpb.NewStringValue("cool-resource-42"),
+								}}),
+							}},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			d, err := AsState(tc.args.xr, tc.args.xc, tc.args.rs)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nState(...): -want, +got:\n%s", tc.reason, diff)
+			}
+
+			if diff := cmp.Diff(tc.want.d, d, protocmp.Transform()); diff != "" {
+				t.Errorf("\n%s\nState(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestGarbageCollectComposedResources(t *testing.T) {
+	errBoom := errors.New("boom")
+
+	type params struct {
+		client client.Writer
+	}
+
+	type args struct {
+		ctx      context.Context
+		owner    metav1.Object
+		observed ComposedResourceStates
+		desired  ComposedResourceStates
+	}
+
+	type want struct {
+		err error
+	}
+
+	cases := map[string]struct {
+		reason string
+		params params
+		args   args
+		want   want
+	}{
+		"UncontrolledResource": {
+			reason: "Resources the XR doesn't control should not be deleted.",
+			params: params{
+				client: &test.MockClient{
+					// We know Delete wasn't called because it's a nil function
+					// and would thus panic if it was.
+				},
+			},
+			args: args{
+				owner: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						UID: "cool-xr",
+					},
+				},
+				observed: ComposedResourceStates{
+					"undesired-resource": ComposedResourceState{Resource: &fake.Composed{}},
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"DeleteError": {
+			reason: "We should return any error encountered deleting the resource.",
+			params: params{
+				client: &test.MockClient{
+					MockDelete: test.NewMockDeleteFn(errBoom),
+				},
+			},
+			args: args{
+				owner: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						UID: "cool-xr",
+					},
+				},
+				observed: ComposedResourceStates{
+					"undesired-resource": ComposedResourceState{
+						Resource: &fake.Composed{
+							ObjectMeta: metav1.ObjectMeta{
+								// This resource is controlled by the XR.
+								OwnerReferences: []metav1.OwnerReference{{
+									Controller: pointer.Bool(true),
+									UID:        "cool-xr",
+								}},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: errors.Wrapf(errBoom, errFmtDeleteCD, "undesired-resource", "", ""),
+			},
+		},
+		"SuccessfulDelete": {
+			reason: "We should successfully delete an observed resource from the API server if it is not desired.",
+			params: params{
+				client: &test.MockClient{
+					MockDelete: test.NewMockDeleteFn(nil),
+				},
+			},
+			args: args{
+				owner: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						UID: "cool-xr",
+					},
+				},
+				observed: ComposedResourceStates{
+					"undesired-resource": ComposedResourceState{
+						Resource: &fake.Composed{
+							ObjectMeta: metav1.ObjectMeta{
+								// This resource is controlled by the XR.
+								OwnerReferences: []metav1.OwnerReference{{
+									Controller: pointer.Bool(true),
+									UID:        "cool-xr",
+								}},
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"SuccessfulNoop": {
+			reason: "We should not delete an observed resource from the API server if it is desired.",
+			params: params{
+				client: &test.MockClient{
+					// We know Delete wasn't called because it's nil and would
+					// panic if it was.
+				},
+			},
+			args: args{
+				owner: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						UID: "cool-xr",
+					},
+				},
+				observed: ComposedResourceStates{
+					"desired-resource": ComposedResourceState{
+						Resource: &fake.Composed{
+							ObjectMeta: metav1.ObjectMeta{
+								// This resource is controlled by the XR.
+								OwnerReferences: []metav1.OwnerReference{{
+									Controller: pointer.Bool(true),
+									UID:        "cool-xr",
+								}},
+							},
+						},
+					},
+				},
+				desired: ComposedResourceStates{
+					// The observed resource above is also desired.
+					"desired-resource": ComposedResourceState{},
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+
+			d := NewDeletingComposedResourceGarbageCollector(tc.params.client)
+			err := d.GarbageCollectComposedResources(tc.args.ctx, tc.args.owner, tc.args.observed, tc.args.desired)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nGarbageCollectComposedResources(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestUpdateResourceRefs(t *testing.T) {
+	type args struct {
+		xr  resource.ComposedResourcesReferencer
+		drs ComposedResourceStates
+	}
+
+	type want struct {
+		xr resource.ComposedResourcesReferencer
+	}
+
+	cases := map[string]struct {
+		reason string
+		args   args
+		want   want
+	}{
+		"Success": {
+			reason: "We should return a consistently ordered set of references.",
+			args: args{
+				xr: &fake.Composite{},
+				drs: ComposedResourceStates{
+					"never-created-c": ComposedResourceState{
+						Resource: &fake.Composed{
+							ObjectMeta: metav1.ObjectMeta{
+								Name: "never-created-c-42",
+							},
+						},
+					},
+					"never-created-b": ComposedResourceState{
+						Resource: &fake.Composed{
+							ObjectMeta: metav1.ObjectMeta{
+								Name: "never-created-b-42",
+							},
+						},
+					},
+					"never-created-a": ComposedResourceState{
+						Resource: &fake.Composed{
+							ObjectMeta: metav1.ObjectMeta{
+								Name: "never-created-a-42",
+							},
+						},
+					},
+				},
+			},
+			want: want{
+				xr: &fake.Composite{
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
+						Refs: []corev1.ObjectReference{
+							{Name: "never-created-a-42"},
+							{Name: "never-created-b-42"},
+							{Name: "never-created-c-42"},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+
+			UpdateResourceRefs(tc.args.xr, tc.args.drs)
+
+			if diff := cmp.Diff(tc.want.xr, tc.args.xr); diff != "" {
+				t.Errorf("\n%s\nUpdateResourceRefs(...): -want, +got:\n%s", tc.reason, diff)
+			}
+
+		})
+	}
+}
diff --git a/internal/controller/apiextensions/composite/composition_pt_test.go b/internal/controller/apiextensions/composite/composition_pt_test.go
index 178ec0918..6b6fe010b 100644
--- a/internal/controller/apiextensions/composite/composition_pt_test.go
+++ b/internal/controller/apiextensions/composite/composition_pt_test.go
@@ -28,7 +28,6 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/json"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -46,6 +45,7 @@ import (
 func TestPTCompose(t *testing.T) {
 	errBoom := errors.New("boom")
 	details := managed.ConnectionDetails{"a": []byte("b")}
+	base := runtime.RawExtension{Raw: []byte(`{"apiVersion":"test.crossplane.io/v1","kind":"ComposedResource"}`)}
 
 	type params struct {
 		kube client.Client
@@ -107,53 +107,45 @@ func TestPTCompose(t *testing.T) {
 				err: errors.Wrap(errBoom, errAssociate),
 			},
 		},
-		// TODO(negz): Test handling of ApplyEnvironmentPatch errors.
-		"RenderComposedError": {
-			reason: "We should include any error encountered while rendering a composed resource as a warning, not as the returned error.",
+		"ParseComposedResourceBaseError": {
+			reason: "We should return any error encountered while parsing a composed resource base template",
 			params: params{
-				kube: &test.MockClient{
-					MockUpdate: test.NewMockUpdateFn(nil),
-
-					// Apply uses Get and Patch.
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
-				},
 				o: []PTComposerOption{
 					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
-						tas := []TemplateAssociation{{
-							Template: v1.ComposedTemplate{
-								Name: pointer.String("cool-resource"),
+						tas := []TemplateAssociation{
+							{
+								Template: v1.ComposedTemplate{
+									Name: pointer.String("uncool-resource"),
+									Base: runtime.RawExtension{Raw: []byte("{}")}, // An invalid, empty base resource template.
+								},
 							},
-						}}
+						}
 						return tas, nil
 					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return errBoom
-					})),
-					WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error { return nil })),
+					WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
 					})),
 					WithComposedConnectionDetailsExtractor(ConnectionDetailsExtractorFn(func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
-						return nil, nil
+						return details, nil
+					})),
+					WithComposedReadinessChecker(ReadinessCheckerFn(func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
+						return true, nil
 					})),
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
+				},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
 			},
 			want: want{
-				res: CompositionResult{
-					Composed: []ComposedResource{{
-						ResourceName: "cool-resource",
-					}},
-					ConnectionDetails: managed.ConnectionDetails{},
-					Events: []event.Event{
-						event.Warning(reasonCompose, errors.Wrapf(errBoom, errFmtResourceName, "cool-resource")),
-					},
-				},
+				err: errors.Wrapf(errors.Wrap(errors.New("Object 'Kind' is missing in '{}'"), errUnmarshalJSON), errFmtParseBase, "uncool-resource"),
 			},
 		},
 		"UpdateCompositeError": {
@@ -167,17 +159,20 @@ func TestPTCompose(t *testing.T) {
 						tas := []TemplateAssociation{{
 							Template: v1.ComposedTemplate{
 								Name: pointer.String("cool-resource"),
+								Base: base,
 							},
 						}}
 						return tas, nil
 					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error { return nil })),
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
+				},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
@@ -192,68 +187,34 @@ func TestPTCompose(t *testing.T) {
 				kube: &test.MockClient{
 					MockUpdate: test.NewMockUpdateFn(nil),
 
-					// Apply calls Get.
-					MockGet: test.NewMockGetFn(errBoom),
+					// Apply calls Create because GenerateName is set.
+					MockCreate: test.NewMockCreateFn(errBoom),
 				},
 				o: []PTComposerOption{
 					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
 						tas := []TemplateAssociation{{
 							Template: v1.ComposedTemplate{
 								Name: pointer.String("cool-resource"),
+								Base: base,
 							},
 						}}
 						return tas, nil
 					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error { return nil })),
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errors.Wrap(errBoom, "cannot get object"), errApply),
-			},
-		},
-		"CompositeRenderError": {
-			reason: "We should return any error encountered while rendering the Composite.",
-			params: params{
-				kube: &test.MockClient{
-					MockUpdate: test.NewMockUpdateFn(nil),
-
-					// Apply calls Get and Patch
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
-				},
-				o: []PTComposerOption{
-					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
-						tas := []TemplateAssociation{{
-							Template: v1.ComposedTemplate{
-								Name: pointer.String("cool-resource"),
-							},
-						}}
-						return tas, nil
-					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
-					WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return errBoom
-					})),
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
 				},
-			},
-			args: args{
-				xr: &fake.Composite{},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
 			},
 			want: want{
-				err: errors.Wrap(errBoom, errRenderCR),
+				err: errors.Wrap(errors.Wrap(errBoom, "cannot create object"), errApplyComposed),
 			},
 		},
 		"FetchConnectionDetailsError": {
@@ -262,32 +223,31 @@ func TestPTCompose(t *testing.T) {
 				kube: &test.MockClient{
 					MockUpdate: test.NewMockUpdateFn(nil),
 
-					// Apply calls Get and Patch
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
+					// Apply calls Create because GenerateName is set.
+					MockCreate: test.NewMockCreateFn(nil),
 				},
 				o: []PTComposerOption{
 					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
 						tas := []TemplateAssociation{{
 							Template: v1.ComposedTemplate{
 								Name: pointer.String("cool-resource"),
+								Base: base,
 							},
 						}}
 						return tas, nil
 					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
-					WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error { return nil })),
 					WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
 						return nil, errBoom
 					})),
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
+				},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
@@ -302,25 +262,20 @@ func TestPTCompose(t *testing.T) {
 				kube: &test.MockClient{
 					MockUpdate: test.NewMockUpdateFn(nil),
 
-					// Apply calls Get and Patch
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
+					// Apply calls Create because GenerateName is set.
+					MockCreate: test.NewMockCreateFn(nil),
 				},
 				o: []PTComposerOption{
 					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
 						tas := []TemplateAssociation{{
 							Template: v1.ComposedTemplate{
 								Name: pointer.String("cool-resource"),
+								Base: base,
 							},
 						}}
 						return tas, nil
 					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
-					WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error { return nil })),
 					WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
 						return nil, nil
 					})),
@@ -330,13 +285,17 @@ func TestPTCompose(t *testing.T) {
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
+				},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
 			},
 			want: want{
-				err: errors.Wrap(errBoom, errExtractDetails),
+				err: errors.Wrapf(errBoom, errFmtExtractDetails, "cool-resource"),
 			},
 		},
 		"CheckReadinessError": {
@@ -345,25 +304,20 @@ func TestPTCompose(t *testing.T) {
 				kube: &test.MockClient{
 					MockUpdate: test.NewMockUpdateFn(nil),
 
-					// Apply calls Get and Patch
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
+					// Apply calls Create because GenerateName is set.
+					MockCreate: test.NewMockCreateFn(nil),
 				},
 				o: []PTComposerOption{
 					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
 						tas := []TemplateAssociation{{
 							Template: v1.ComposedTemplate{
 								Name: pointer.String("cool-resource"),
+								Base: base,
 							},
 						}}
 						return tas, nil
 					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
-					WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error { return nil })),
 					WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
 						return nil, nil
 					})),
@@ -376,13 +330,17 @@ func TestPTCompose(t *testing.T) {
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
+				},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
 			},
 			want: want{
-				err: errors.Wrap(errBoom, errReadiness),
+				err: errors.Wrapf(errBoom, errFmtCheckReadiness, "cool-resource"),
 			},
 		},
 		"CompositeApplyError": {
@@ -401,13 +359,14 @@ func TestPTCompose(t *testing.T) {
 					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
 						return nil, nil
 					})),
-					WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
+				},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
@@ -422,25 +381,22 @@ func TestPTCompose(t *testing.T) {
 				kube: &test.MockClient{
 					MockUpdate: test.NewMockUpdateFn(nil),
 
-					// Apply uses Get and Patch.
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
+					// Apply uses Get, Create, and Patch.
+					MockGet:    test.NewMockGetFn(nil),
+					MockCreate: test.NewMockCreateFn(nil),
+					MockPatch:  test.NewMockPatchFn(nil),
 				},
 				o: []PTComposerOption{
 					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
 						tas := []TemplateAssociation{{
 							Template: v1.ComposedTemplate{
 								Name: pointer.String("cool-resource"),
+								Base: base,
 							},
 						}}
 						return tas, nil
 					})),
-					WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
-					WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-						return nil
-					})),
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error { return nil })),
 					WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
 						return nil, nil
 					})),
@@ -453,7 +409,11 @@ func TestPTCompose(t *testing.T) {
 				},
 			},
 			args: args{
-				xr: &fake.Composite{},
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
+					},
+				},
 				req: CompositionRequest{
 					Revision: &v1.CompositionRevision{},
 				},
@@ -468,159 +428,99 @@ func TestPTCompose(t *testing.T) {
 				},
 			},
 		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			c := NewPTComposer(tc.params.kube, tc.params.o...)
-			res, err := c.Compose(tc.args.ctx, tc.args.xr, tc.args.req)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.res, res, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestRender(t *testing.T) {
-	ctrl := true
-	tmpl, _ := json.Marshal(&fake.Managed{})
-	errBoom := errors.New("boom")
+		"PartialSuccess": {
+			reason: "We should return the resources we composed, and our derived connection details. We should return events for any resources we couldn't compose",
+			params: params{
+				kube: &test.MockClient{
+					MockUpdate: test.NewMockUpdateFn(nil),
 
-	type args struct {
-		ctx context.Context
-		cp  resource.Composite
-		cd  resource.Composed
-		t   v1.ComposedTemplate
-	}
-	type want struct {
-		cd  resource.Composed
-		err error
-	}
-	cases := map[string]struct {
-		reason string
-		client client.Client
-		args
-		want
-	}{
-		"InvalidTemplate": {
-			reason: "Invalid template should not be accepted",
-			args: args{
-				cd: &fake.Composed{},
-				t:  v1.ComposedTemplate{Base: runtime.RawExtension{Raw: []byte("olala")}},
-			},
-			want: want{
-				cd:  &fake.Composed{},
-				err: errors.Wrap(errors.New("invalid character 'o' looking for beginning of value"), errUnmarshal),
-			},
-		},
-		"NoLabel": {
-			reason: "The name prefix label has to be set",
-			args: args{
-				cp: &fake.Composite{},
-				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{Name: "cd"}},
-				t:  v1.ComposedTemplate{Base: runtime.RawExtension{Raw: tmpl}},
-			},
-			want: want{
-				cd:  &fake.Composed{ObjectMeta: metav1.ObjectMeta{Name: "cd"}},
-				err: errors.New(errNamePrefix),
+					// Apply uses Get, Create, and Patch.
+					MockGet:    test.NewMockGetFn(nil),
+					MockCreate: test.NewMockCreateFn(nil),
+					MockPatch:  test.NewMockPatchFn(nil),
+				},
+				o: []PTComposerOption{
+					WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
+						tas := []TemplateAssociation{
+							{
+								Template: v1.ComposedTemplate{
+									Name: pointer.String("cool-resource"),
+									Base: base,
+								},
+							},
+							{
+								// This resource won't apply successfully due to
+								// the clause below in the dry-run renderer.
+								Template: v1.ComposedTemplate{
+									Name: pointer.String("uncool-resource"),
+									Base: runtime.RawExtension{Raw: []byte(`{"apiVersion":"test.crossplane.io/v1","kind":"BrokenResource"}`)},
+								},
+							},
+						}
+						return tas, nil
+					})),
+					WithComposedDryRunRenderer(DryRunRendererFn(func(ctx context.Context, cd resource.Object) error {
+						if cd.GetObjectKind().GroupVersionKind().Kind == "BrokenResource" {
+							return errBoom
+						}
+						return nil
+					})),
+					WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
+						return nil, nil
+					})),
+					WithComposedConnectionDetailsExtractor(ConnectionDetailsExtractorFn(func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
+						return details, nil
+					})),
+					WithComposedReadinessChecker(ReadinessCheckerFn(func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
+						return true, nil
+					})),
+				},
 			},
-		},
-		"DryRunError": {
-			reason: "Errors dry-run creating the rendered resource to name it should be returned",
-			client: &test.MockClient{MockCreate: test.NewMockCreateFn(errBoom)},
 			args: args{
-				cp: &fake.Composite{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
-					xcrd.LabelKeyNamePrefixForComposed: "ola",
-					xcrd.LabelKeyClaimName:             "rola",
-					xcrd.LabelKeyClaimNamespace:        "rolans",
-				}}},
-				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{}},
-				t:  v1.ComposedTemplate{Base: runtime.RawExtension{Raw: tmpl}},
-			},
-			want: want{
-				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
-					GenerateName: "ola-",
-					Labels: map[string]string{
-						xcrd.LabelKeyNamePrefixForComposed: "ola",
-						xcrd.LabelKeyClaimName:             "rola",
-						xcrd.LabelKeyClaimNamespace:        "rolans",
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{xcrd.LabelKeyNamePrefixForComposed: "cool-xr"},
 					},
-					OwnerReferences: []metav1.OwnerReference{{Controller: &ctrl, BlockOwnerDeletion: &ctrl}},
-				}},
-				err: errors.Wrap(errBoom, errName),
-			},
-		},
-		"ControllerError": {
-			reason: "External controller owner references should cause an exception",
-			client: &test.MockClient{MockCreate: test.NewMockCreateFn(nil)},
-			args: args{
-				cp: &fake.Composite{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
-					xcrd.LabelKeyNamePrefixForComposed: "ola",
-					xcrd.LabelKeyClaimName:             "rola",
-					xcrd.LabelKeyClaimNamespace:        "rolans",
-				}}},
-				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{Name: "cd",
-					OwnerReferences: []metav1.OwnerReference{{Controller: &ctrl, BlockOwnerDeletion: &ctrl,
-						UID: "random_uid"}}}},
-				t: v1.ComposedTemplate{Base: runtime.RawExtension{Raw: tmpl}},
+				},
+				req: CompositionRequest{
+					Revision: &v1.CompositionRevision{},
+				},
 			},
 			want: want{
-				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
-					Name:         "cd",
-					GenerateName: "ola-",
-					Labels: map[string]string{
-						xcrd.LabelKeyNamePrefixForComposed: "ola",
-						xcrd.LabelKeyClaimName:             "rola",
-						xcrd.LabelKeyClaimNamespace:        "rolans",
+				res: CompositionResult{
+					Composed: []ComposedResource{
+						{
+							ResourceName: "cool-resource",
+							Ready:        true,
+						},
+						{
+							ResourceName: "uncool-resource",
+							Ready:        false,
+						},
 					},
-					OwnerReferences: []metav1.OwnerReference{{Controller: &ctrl, BlockOwnerDeletion: &ctrl,
-						UID: "random_uid"}},
-				}},
-				err: errors.Wrap(errors.Errorf("cd is already controlled by   (UID random_uid)"), errSetControllerRef),
-			},
-		},
-		"Success": {
-			reason: "Configuration should result in the right object with correct generateName",
-			client: &test.MockClient{MockCreate: test.NewMockCreateFn(nil)},
-			args: args{
-				cp: &fake.Composite{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
-					xcrd.LabelKeyNamePrefixForComposed: "ola",
-					xcrd.LabelKeyClaimName:             "rola",
-					xcrd.LabelKeyClaimNamespace:        "rolans",
-				}}},
-				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{Name: "cd"}},
-				t:  v1.ComposedTemplate{Base: runtime.RawExtension{Raw: tmpl}},
-			},
-			want: want{
-				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
-					Name:         "cd",
-					GenerateName: "ola-",
-					Labels: map[string]string{
-						xcrd.LabelKeyNamePrefixForComposed: "ola",
-						xcrd.LabelKeyClaimName:             "rola",
-						xcrd.LabelKeyClaimNamespace:        "rolans",
+					ConnectionDetails: details,
+					Events: []event.Event{
+						event.Warning(reasonCompose, errors.Wrapf(errBoom, errFmtDryRunApply, "uncool-resource")),
 					},
-					OwnerReferences: []metav1.OwnerReference{{Controller: &ctrl, BlockOwnerDeletion: &ctrl}},
-				}},
+				},
 			},
 		},
 	}
+
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
-			r := NewAPIDryRunRenderer(tc.client)
-			err := r.Render(tc.args.ctx, tc.args.cp, tc.args.cd, tc.args.t, nil)
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
+
+			c := NewPTComposer(tc.params.kube, tc.params.o...)
+			res, err := c.Compose(tc.args.ctx, tc.args.xr, tc.args.req)
+
+			if diff := cmp.Diff(tc.want.res, res, cmpopts.EquateEmpty()); diff != "" {
+				t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
 			}
-			if diff := cmp.Diff(tc.want.cd, tc.args.cd); diff != "" {
-				t.Errorf("\n%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nCompose(...): -want error, +got error:\n%s", tc.reason, diff)
 			}
+
 		})
 	}
 }
@@ -765,7 +665,7 @@ func TestGarbageCollectingAssociator(t *testing.T) {
 			reason: "We should associate referenced resources by their template name annotation.",
 			c: &test.MockClient{
 				MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-					SetCompositionResourceName(obj.(metav1.Object), n0)
+					SetCompositionResourceName(obj.(metav1.Object), ResourceName(n0))
 					return nil
 				}),
 			},
@@ -779,8 +679,8 @@ func TestGarbageCollectingAssociator(t *testing.T) {
 				tas: []TemplateAssociation{{Template: t0, Reference: r0}},
 			},
 		},
-		"UncontrolledResource": {
-			reason: "We should not garbage collect a resource that we don't control.",
+		"ResourceControlledBySomeoneElse": {
+			reason: "We should not garbage collect a resource that is controlled by another resource.",
 			c: &test.MockClient{
 				MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
 					// The template used to create this resource is no longer known to us.
@@ -807,18 +707,49 @@ func TestGarbageCollectingAssociator(t *testing.T) {
 				tas: []TemplateAssociation{{Template: t0}},
 			},
 		},
+		"ResourceNotControlled": {
+			reason: "We should not garbage collect a resource that has no controller reference.",
+			c: &test.MockClient{
+				MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
+					// The template used to create this resource is no longer known to us.
+					SetCompositionResourceName(obj, "unknown")
+
+					// This resource is not controlled by anyone.
+					return nil
+				}),
+			},
+			args: args{
+				cr: &fake.Composite{
+					ObjectMeta:                  metav1.ObjectMeta{UID: types.UID("very-unique")},
+					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{Refs: []corev1.ObjectReference{r0}},
+				},
+				ct: []v1.ComposedTemplate{t0},
+			},
+			want: want{
+				tas: []TemplateAssociation{{Template: t0}},
+			},
+		},
 		"GarbageCollectionError": {
 			reason: "We should return errors encountered while garbage collecting a composed resource.",
 			c: &test.MockClient{
 				MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
 					// The template used to create this resource is no longer known to us.
 					SetCompositionResourceName(obj, "unknown")
+
+					// This resource is controlled by us.
+					ctrl := true
+					obj.SetOwnerReferences([]metav1.OwnerReference{{
+						Controller:         &ctrl,
+						BlockOwnerDeletion: &ctrl,
+						UID:                types.UID("it-me"),
+					}})
 					return nil
 				}),
 				MockDelete: test.NewMockDeleteFn(errBoom),
 			},
 			args: args{
 				cr: &fake.Composite{
+					ObjectMeta:                  metav1.ObjectMeta{UID: "it-me"},
 					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{Refs: []corev1.ObjectReference{r0}},
 				},
 				ct: []v1.ComposedTemplate{t0},
@@ -833,12 +764,22 @@ func TestGarbageCollectingAssociator(t *testing.T) {
 				MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
 					// The template used to create this resource is no longer known to us.
 					SetCompositionResourceName(obj, "unknown")
+
+					// This resource is controlled by us.
+					ctrl := true
+					obj.SetOwnerReferences([]metav1.OwnerReference{{
+						Controller:         &ctrl,
+						BlockOwnerDeletion: &ctrl,
+						UID:                types.UID("it-me"),
+					}})
+
 					return nil
 				}),
 				MockDelete: test.NewMockDeleteFn(nil),
 			},
 			args: args{
 				cr: &fake.Composite{
+					ObjectMeta:                  metav1.ObjectMeta{UID: "it-me"},
 					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{Refs: []corev1.ObjectReference{r0}},
 				},
 				ct: []v1.ComposedTemplate{t0},
diff --git a/internal/controller/apiextensions/composite/composition_ptf_test.go b/internal/controller/apiextensions/composite/composition_ptf_test.go
deleted file mode 100644
index f7869a469..000000000
--- a/internal/controller/apiextensions/composite/composition_ptf_test.go
+++ /dev/null
@@ -1,2499 +0,0 @@
-/*
-Copyright 2022 The Crossplane Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package composite
-
-import (
-	"context"
-	"net"
-	"sync"
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-cmp/cmp/cmpopts"
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/status"
-	"google.golang.org/protobuf/testing/protocmp"
-	corev1 "k8s.io/api/core/v1"
-	kerrors "k8s.io/apimachinery/pkg/api/errors"
-	computeresource "k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/json"
-	"k8s.io/utils/pointer"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/yaml"
-
-	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
-	"github.com/crossplane/crossplane-runtime/pkg/errors"
-	"github.com/crossplane/crossplane-runtime/pkg/event"
-	"github.com/crossplane/crossplane-runtime/pkg/meta"
-	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
-	"github.com/crossplane/crossplane-runtime/pkg/resource"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
-	"github.com/crossplane/crossplane-runtime/pkg/test"
-
-	iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
-	fnpbv1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1alpha1"
-	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
-	"github.com/crossplane/crossplane/internal/xcrd"
-)
-
-func TestPTFCompose(t *testing.T) {
-	errBoom := errors.New("boom")
-	details := managed.ConnectionDetails{"a": []byte("b")}
-
-	unmarshalable := kunstructured.Unstructured{Object: map[string]interface{}{
-		"you-cant-marshal-a-channel": make(chan<- string),
-	}}
-	_, errUnmarshalableXR := json.Marshal(&composite.Unstructured{Unstructured: unmarshalable})
-
-	type params struct {
-		kube client.Client
-		o    []PTFComposerOption
-	}
-	type args struct {
-		ctx context.Context
-		xr  resource.Composite
-		req CompositionRequest
-	}
-	type want struct {
-		res CompositionResult
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"FetchConnectionError": {
-			reason: "We should return any error encountered while fetching the XR's connection details.",
-			params: params{
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, errBoom
-					})),
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errFetchXRConnectionDetails),
-			},
-		},
-		"GetComposedResourcesError": {
-			reason: "We should return any error encountered while getting the XR's existing composed resources.",
-			params: params{
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, errBoom
-					})),
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errGetExistingCDs),
-			},
-		},
-		"FunctionIOObservedError": {
-			reason: "We should return any error encountered while building a FunctionIO observed object.",
-			params: params{
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, nil
-					})),
-				},
-			},
-			args: args{
-				xr: &composite.Unstructured{Unstructured: unmarshalable},
-			},
-			want: want{
-				err: errors.Wrap(errors.Wrap(errUnmarshalableXR, errMarshalXR), errBuildFunctionIOObserved),
-			},
-		},
-		"PatchAndTransformError": {
-			reason: "We should return any error encountered while running patches and transforms.",
-			params: params{
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return errBoom
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errPatchAndTransform),
-			},
-		},
-		"FunctionIODesiredError": {
-			reason: "We should return any error encountered while building a FunctionIO desired object.",
-			params: params{
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						// After FunctionIOObserved has been called we replace
-						// our XR with one that can't be marshalled by
-						// FunctionIODesired.
-						s.Composite = &composite.Unstructured{Unstructured: unmarshalable}
-						return nil
-					})),
-				},
-			},
-			args: args{
-				// We start with an XR that can be marshalled (i.e. by
-				// FunctionIOObserved).
-				xr: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrap(errors.Wrap(errUnmarshalableXR, errMarshalXR), errBuildFunctionIODesired),
-			},
-		},
-		"RunFunctionPipelineError": {
-			reason: "We should return any error encountered while running the Composition Function pipeline.",
-			params: params{
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithFunctionPipelineRunner(FunctionPipelineRunnerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-						return errBoom
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errRunFunctionPipeline),
-			},
-		},
-		"DeleteComposedResourcesError": {
-			reason: "We should return any error encountered while deleting undesired composed resources.",
-			params: params{
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithFunctionPipelineRunner(FunctionPipelineRunnerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-						return nil
-					})),
-					WithComposedResourceDeleter(ComposedResourceDeleterFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return errBoom
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errDeleteUndesiredCDs),
-			},
-		},
-		"ApplyCompositeError": {
-			reason: "We should return any error encountered while applying the XR.",
-			params: params{
-				kube: &test.MockClient{
-					// We test through the ClientApplicator - Get is called by Apply.
-					MockGet: test.NewMockGetFn(errBoom),
-				},
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithFunctionPipelineRunner(FunctionPipelineRunnerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-						return nil
-					})),
-					WithComposedResourceDeleter(ComposedResourceDeleterFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return nil
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrap(errors.Wrap(errBoom, "cannot get object"), errApplyXR),
-			},
-		},
-		"DoNotApplyFailedRenders": {
-			reason: "We should skip applying a composed resource if we failed to render its P&T template.",
-			params: params{
-				kube: &test.MockClient{
-					// We test through the ClientApplicator - Get is called by Apply.
-					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-						// Only return an error if this is a composed resource.
-						if _, ok := obj.(*fake.Composed); ok {
-							// We don't want to return this - if we did it would
-							// imply we called Apply.
-							return errBoom
-						}
-						return nil
-					}),
-					// The ClientApplicator will call Update for the XR.
-					MockPatch: test.NewMockPatchFn(nil),
-				},
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						cds := ComposedResourceStates{
-							"cool-resource": ComposedResourceState{
-								ComposedResource: ComposedResource{
-									ResourceName: "cool-resource",
-								},
-								Resource:          &fake.Composed{},
-								Template:          &v1.ComposedTemplate{},
-								TemplateRenderErr: errBoom,
-							},
-						}
-						return cds, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithFunctionPipelineRunner(FunctionPipelineRunnerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-						return nil
-					})),
-					WithComposedResourceDeleter(ComposedResourceDeleterFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return nil
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				res: CompositionResult{Composed: []ComposedResource{{ResourceName: "cool-resource"}}},
-				err: nil,
-			},
-		},
-		"ApplyComposedError": {
-			reason: "We should return any error encountered while applying a composed resource.",
-			params: params{
-				kube: &test.MockClient{
-					// We test through the ClientApplicator - Get is called by Apply.
-					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-						// Only return an error if this is a composed resource.
-						if _, ok := obj.(*fake.Composed); ok {
-							return errBoom
-						}
-						return nil
-					}),
-					// The ClientApplicator will call Update for the XR.
-					MockPatch: test.NewMockPatchFn(nil),
-				},
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						cds := ComposedResourceStates{
-							"cool-resource": ComposedResourceState{
-								ComposedResource: ComposedResource{
-									ResourceName: "cool-resource",
-								},
-								Resource: &fake.Composed{},
-								Template: &v1.ComposedTemplate{},
-							},
-						}
-						return cds, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithFunctionPipelineRunner(FunctionPipelineRunnerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-						return nil
-					})),
-					WithComposedResourceDeleter(ComposedResourceDeleterFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return nil
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrapf(errors.Wrap(errBoom, "cannot get object"), errFmtApplyCD, "cool-resource"),
-			},
-		},
-		"ObserveComposedResourcesError": {
-			reason: "We should return any error encountered while observing a composed resource.",
-			params: params{
-				kube: &test.MockClient{
-					// These are both called by Apply.
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
-				},
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return nil, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						return nil, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithFunctionPipelineRunner(FunctionPipelineRunnerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-						return nil
-					})),
-					WithComposedResourceDeleter(ComposedResourceDeleterFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return errBoom
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errObserveCDs),
-			},
-		},
-		"Success": {
-			reason: "",
-			params: params{
-				kube: &test.MockClient{
-					// These are both called by Apply.
-					MockGet:   test.NewMockGetFn(nil),
-					MockPatch: test.NewMockPatchFn(nil),
-				},
-				o: []PTFComposerOption{
-					WithCompositeConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-						return details, nil
-					})),
-					WithComposedResourceGetter(ComposedResourceGetterFn(func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
-						cds := ComposedResourceStates{
-							"cool-resource": ComposedResourceState{
-								ComposedResource: ComposedResource{
-									ResourceName: "cool-resource",
-								},
-								Resource: &fake.Composed{},
-							},
-						}
-						return cds, nil
-					})),
-					WithPatchAndTransformer(PatchAndTransformerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithFunctionPipelineRunner(FunctionPipelineRunnerFn(func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
-						return nil
-					})),
-					WithComposedResourceDeleter(ComposedResourceDeleterFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return nil
-					})),
-					WithComposedResourceObserver(ComposedResourceObserverFn(func(ctx context.Context, s *PTFCompositionState) error {
-						return nil
-					})),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{},
-			},
-			want: want{
-				res: CompositionResult{
-					Composed: []ComposedResource{{
-						ResourceName: "cool-resource",
-					}},
-					ConnectionDetails: details,
-				},
-				err: nil,
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			c := NewPTFComposer(tc.params.kube, tc.params.o...)
-			res, err := c.Compose(tc.args.ctx, tc.args.xr, tc.args.req)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.res, res, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestGetComposedResources(t *testing.T) {
-	errBoom := errors.New("boom")
-	details := managed.ConnectionDetails{"a": []byte("b")}
-
-	type params struct {
-		c client.Reader
-		f managed.ConnectionDetailsFetcher
-	}
-
-	type args struct {
-		ctx context.Context
-		xr  resource.Composite
-	}
-
-	type want struct {
-		cds ComposedResourceStates
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"UnnamedRef": {
-			reason: "We should skip any resource references without names.",
-			params: params{
-				c: &test.MockClient{
-					// We should continue past the unnamed reference and not hit
-					// this error.
-					MockGet: test.NewMockGetFn(errBoom),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{
-							{
-								APIVersion: "example.org/v1",
-								Kind:       "Unnamed",
-							},
-						},
-					},
-				},
-			},
-		},
-		"ComposedResourceNotFound": {
-			reason: "We should skip any resources that are not found.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{
-							{Name: "cool-resource"},
-						},
-					},
-				},
-			},
-		},
-		"GetComposedResourceError": {
-			reason: "We should return any error we encounter while getting a composed resource.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(errBoom),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{
-							{Name: "cool-resource"},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errGetComposed),
-			},
-		},
-		"UncontrolledComposedResource": {
-			reason: "We should skip any composed resources our XR doesn't control.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-						_ = meta.AddControllerReference(obj, metav1.OwnerReference{
-							UID:        types.UID("someone-else"),
-							Controller: pointer.Bool(true),
-						})
-
-						return nil
-					}),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{
-							{Name: "cool-resource"},
-						},
-					},
-				},
-			},
-			want: want{
-				err: nil,
-			},
-		},
-		"AnonymousComposedResourceError": {
-			reason: "We should return an error if we encounter an (unsupported) anonymous composed resource.",
-			params: params{
-				c: &test.MockClient{
-					// We 'return' an empty resource with no annotations.
-					MockGet: test.NewMockGetFn(nil),
-				},
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{
-							{Name: "cool-resource"},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.New(errAnonymousCD),
-			},
-		},
-		"FetchConnectionDetailsError": {
-			reason: "We should return an error if we can't fetch composed resource connection details.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-						obj.SetName("cool-resource-42")
-						SetCompositionResourceName(obj, "cool-resource")
-						return nil
-					}),
-				},
-				f: ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-					return nil, errBoom
-				}),
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{
-							{
-								Kind: "Broken",
-								Name: "cool-resource-42",
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrapf(errBoom, errFmtFetchCDConnectionDetails, "cool-resource", "Broken", "cool-resource-42"),
-			},
-		},
-		"Success": {
-			reason: "We should return any composed resources and their connection details.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-						obj.SetName("cool-resource-42")
-						SetCompositionResourceName(obj, "cool-resource")
-						return nil
-					}),
-				},
-				f: ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
-					return details, nil
-				}),
-			},
-			args: args{
-				xr: &fake.Composite{
-					ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-						Refs: []corev1.ObjectReference{
-							{
-								APIVersion: "example.org/v1",
-								Kind:       "Composed",
-								Name:       "cool-resource-42",
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				cds: ComposedResourceStates{
-					"cool-resource": ComposedResourceState{
-						ComposedResource: ComposedResource{
-							ResourceName: "cool-resource",
-						},
-						ConnectionDetails: details,
-						Resource: func() resource.Composed {
-							cd := composed.New()
-							cd.SetAPIVersion("example.org/v1")
-							cd.SetKind("Composed")
-							cd.SetName("cool-resource-42")
-							SetCompositionResourceName(cd, "cool-resource")
-							return cd
-						}(),
-					},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			g := NewExistingComposedResourceGetter(tc.params.c, tc.params.f)
-			cds, err := g.GetComposedResources(tc.args.ctx, tc.args.xr)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nGetComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.cds, cds, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nGetComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestFunctionIOObserved(t *testing.T) {
-	unmarshalable := kunstructured.Unstructured{Object: map[string]interface{}{
-		"you-cant-marshal-a-channel": make(chan<- string),
-	}}
-	_, errUnmarshalableXR := json.Marshal(&composite.Unstructured{Unstructured: unmarshalable})
-	_, errUnmarshalableCD := json.Marshal(&composed.Unstructured{Unstructured: unmarshalable})
-
-	type args struct {
-		s *PTFCompositionState
-	}
-	type want struct {
-		o   iov1alpha1.Observed
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		args   args
-		want   want
-	}{
-		"MarshalCompositeError": {
-			reason: "We should return an error if we can't marshal the XR.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: &composite.Unstructured{Unstructured: unmarshalable},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errUnmarshalableXR, errMarshalXR),
-			},
-		},
-		"MarshalComposedError": {
-			reason: "We should return an error if we can't marshal a composed resource.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: composite.New(),
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							Resource: &composed.Unstructured{Unstructured: unmarshalable},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errUnmarshalableCD, errMarshalCD),
-			},
-		},
-		"Success": {
-			reason: "We should successfully build a FunctionIO Observed struct from our state.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: composite.New(composite.WithGroupVersionKind(schema.GroupVersionKind{
-						Group:   "example.org",
-						Version: "v1",
-						Kind:    "Composite",
-					})),
-					ConnectionDetails: managed.ConnectionDetails{"a": []byte("b")},
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{ResourceName: "cool-resource"},
-							Resource: composed.New(composed.FromReference(corev1.ObjectReference{
-								APIVersion: "example.org/v2",
-								Kind:       "Composed",
-								Name:       "cool-resource-42",
-							})),
-							ConnectionDetails: managed.ConnectionDetails{"c": []byte("d")},
-						},
-					},
-				},
-			},
-			want: want{
-				o: iov1alpha1.Observed{
-					Composite: iov1alpha1.ObservedComposite{
-						Resource: runtime.RawExtension{
-							Raw: []byte(`{"apiVersion":"example.org/v1","kind":"Composite"}`),
-						},
-						ConnectionDetails: []iov1alpha1.ExplicitConnectionDetail{{
-							Name:  "a",
-							Value: "b",
-						}},
-					},
-					Resources: []iov1alpha1.ObservedResource{{
-						Name: "cool-resource",
-						Resource: runtime.RawExtension{
-							Raw: []byte(`{"apiVersion":"example.org/v2","kind":"Composed","metadata":{"name":"cool-resource-42"}}`),
-						},
-						ConnectionDetails: []iov1alpha1.ExplicitConnectionDetail{{
-							Name:  "c",
-							Value: "d",
-						}},
-					}},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			o, err := FunctionIOObserved(tc.args.s)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nFunctionIOObserved(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.o, o, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nFunctionIOObserved(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestPatchAndTransform(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type params struct {
-		composite Renderer
-		composed  Renderer
-	}
-
-	type args struct {
-		ctx context.Context
-		req CompositionRequest
-		s   *PTFCompositionState
-	}
-
-	type want struct {
-		s   *PTFCompositionState
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"ComposedTemplatesError": {
-			reason: "We should return any error encountered while inlining a Composition's PatchSets.",
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Resources: []v1.ComposedTemplate{{
-								Patches: []v1.Patch{{
-									// This reference to a non-existent patchset
-									// triggers the error.
-									Type:         v1.PatchTypePatchSet,
-									PatchSetName: pointer.String("nonexistent-patchset"),
-								}},
-							}},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errors.Errorf(errFmtUndefinedPatchSet, "nonexistent-patchset"), errInline),
-			},
-		},
-		// TODO(negz): Test handling of ApplyEnvironmentPatch errors.
-		"CompositeRenderError": {
-			reason: "We should return any error encountered while rendering an XR.",
-			params: params{
-				composite: RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-					return errBoom
-				}),
-			},
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Resources: []v1.ComposedTemplate{
-								{
-									Name: pointer.String("cool-resource"),
-								},
-							},
-						},
-					},
-				},
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						// Corresponds to the ComposedTemplate above. The
-						// resource must exist in order for the code to try
-						// render the XR from it.
-						"cool-resource": ComposedResourceState{
-							Resource: func() *composed.Unstructured {
-								r := composed.New()
-								r.SetKind("Broken")
-								r.SetName("cool-resource-42")
-								return r
-							}(),
-						},
-					},
-				},
-			},
-			want: want{
-				// We expect the state to be unchanged.
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							Resource: func() *composed.Unstructured {
-								r := composed.New()
-								r.SetKind("Broken")
-								r.SetName("cool-resource-42")
-								return r
-							}(),
-						},
-					},
-				},
-				err: errors.Wrapf(errBoom, errFmtRenderXR, "cool-resource", "Broken", "cool-resource-42"),
-			},
-		},
-		"ComposedRenderError": {
-			reason: "We should include any error encountered while rendering a composed resource in our state.",
-			params: params{
-				composite: RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-					return nil
-				}),
-				composed: RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *Environment) error {
-					return errBoom
-				}),
-			},
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Resources: []v1.ComposedTemplate{
-								{
-									Name: pointer.String("cool-resource"),
-								},
-							},
-						},
-					},
-				},
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						// Corresponds to the ComposedTemplate above. The
-						// resource must exist in order for the code to try
-						// render the XR from it.
-						"cool-resource": ComposedResourceState{
-							Resource: func() *composed.Unstructured {
-								r := composed.New()
-								r.SetKind("Broken")
-								r.SetName("cool-resource-42")
-								return r
-							}(),
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: func() *composed.Unstructured {
-								r := composed.New()
-								r.SetKind("Broken")
-								r.SetName("cool-resource-42")
-								return r
-							}(),
-							Template: &v1.ComposedTemplate{
-								Name: pointer.String("cool-resource"),
-							},
-							TemplateRenderErr: errBoom,
-						},
-					},
-					Events: []event.Event{
-						event.Warning(reasonCompose, errors.Wrapf(errBoom, errFmtResourceName, "cool-resource")),
-					},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			pt := NewXRCDPatchAndTransformer(tc.params.composite, tc.params.composed)
-			err := pt.PatchAndTransform(tc.args.ctx, tc.args.req, tc.args.s)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nGetComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			// We need to EquateErrors here for the TemplateRenderError in state.
-			if diff := cmp.Diff(tc.want.s, tc.args.s, cmpopts.EquateEmpty(), test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nGetComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestFunctionIODesired(t *testing.T) {
-	unmarshalable := kunstructured.Unstructured{Object: map[string]interface{}{
-		"you-cant-marshal-a-channel": make(chan<- string),
-	}}
-	_, errUnmarshalableXR := json.Marshal(&composite.Unstructured{Unstructured: unmarshalable})
-	_, errUnmarshalableCD := json.Marshal(&composed.Unstructured{Unstructured: unmarshalable})
-
-	type args struct {
-		s *PTFCompositionState
-	}
-	type want struct {
-		o   iov1alpha1.Desired
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		args   args
-		want   want
-	}{
-		"MarshalCompositeError": {
-			reason: "We should return an error if we can't marshal the XR.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: &composite.Unstructured{Unstructured: unmarshalable},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errUnmarshalableXR, errMarshalXR),
-			},
-		},
-		"MarshalComposedError": {
-			reason: "We should return an error if we can't marshal a composed resource.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: composite.New(),
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							Template: &v1.ComposedTemplate{},
-							Resource: &composed.Unstructured{Unstructured: unmarshalable},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errUnmarshalableCD, errMarshalCD),
-			},
-		},
-		"Success": {
-			reason: "We should successfully build a FunctionIO Desired struct from our state.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: composite.New(composite.WithGroupVersionKind(schema.GroupVersionKind{
-						Group:   "example.org",
-						Version: "v1",
-						Kind:    "Composite",
-					})),
-					ConnectionDetails: managed.ConnectionDetails{"a": []byte("b")},
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{ResourceName: "cool-resource"},
-							Template:         &v1.ComposedTemplate{},
-							Resource: composed.New(composed.FromReference(corev1.ObjectReference{
-								APIVersion: "example.org/v2",
-								Kind:       "Composed",
-								Name:       "cool-resource-42",
-							})),
-						},
-					},
-				},
-			},
-			want: want{
-				o: iov1alpha1.Desired{
-					Composite: iov1alpha1.DesiredComposite{
-						Resource: runtime.RawExtension{
-							Raw: []byte(`{"apiVersion":"example.org/v1","kind":"Composite"}`),
-						},
-						ConnectionDetails: []iov1alpha1.ExplicitConnectionDetail{{
-							Name:  "a",
-							Value: "b",
-						}},
-					},
-					Resources: []iov1alpha1.DesiredResource{{
-						Name: "cool-resource",
-						Resource: runtime.RawExtension{
-							Raw: []byte(`{"apiVersion":"example.org/v2","kind":"Composed","metadata":{"name":"cool-resource-42"}}`),
-						},
-					}},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			o, err := FunctionIODesired(tc.args.s)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nFunctionIODesired(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.o, o, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nFunctionIODesired(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestWithKubernetesAuthentication(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type params struct {
-		c              client.Reader
-		namespace      string
-		serviceAccount string
-		registry       string
-	}
-	type args struct {
-		ctx context.Context
-		fn  *v1.ContainerFunction
-		r   *fnpbv1alpha1.RunFunctionRequest
-	}
-	type want struct {
-		r   *fnpbv1alpha1.RunFunctionRequest
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"GetServiceAccountError": {
-			reason: "We should return an error if we can't get the service account.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(errBoom),
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errGetServiceAccount),
-			},
-		},
-		"GetSecretError": {
-			reason: "We should return an error if we can't get an image pull secret.",
-			params: params{
-				c: &test.MockClient{
-					MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
-						if _, ok := obj.(*corev1.Secret); ok {
-							return errBoom
-						}
-						return nil
-					},
-					),
-				},
-			},
-			args: args{
-				fn: &v1.ContainerFunction{
-					ImagePullSecrets: []corev1.LocalObjectReference{{Name: "cool-secret"}},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errGetImagePullSecret),
-			},
-		},
-		"Success": {
-			reason: "We should successfully parse OCI registry authentication credentials from an image pull secret.",
-			params: params{
-				namespace: "cool-namespace",
-				c: &test.MockClient{
-					MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
-						if s, ok := obj.(*corev1.Secret); ok {
-							want := client.ObjectKey{Namespace: "cool-namespace", Name: "cool-secret"}
-
-							if diff := cmp.Diff(want, key); diff != "" {
-								t.Errorf("\nclient.Get(...): -want key, +got key:\n%s", diff)
-							}
-
-							s.Type = corev1.SecretTypeDockerConfigJson
-							s.Data = map[string][]byte{
-								corev1.DockerConfigJsonKey: []byte(`{"auths":{"xpkg.example.org":{"username":"cool-user","password":"cool-pass"}}}`),
-							}
-						}
-						return nil
-					},
-				},
-				registry: "index.docker.io",
-			},
-			args: args{
-				fn: &v1.ContainerFunction{
-					Image:            "xpkg.example.org/cool-image:v1.0.0",
-					ImagePullSecrets: []corev1.LocalObjectReference{{Name: "cool-secret"}},
-				},
-				r: &fnpbv1alpha1.RunFunctionRequest{},
-			},
-			want: want{
-				r: &fnpbv1alpha1.RunFunctionRequest{
-					ImagePullConfig: &fnpbv1alpha1.ImagePullConfig{
-						Auth: &fnpbv1alpha1.ImagePullAuth{
-							Username: "cool-user",
-							Password: "cool-pass",
-						},
-					},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			err := WithKubernetesAuthentication(tc.params.c, tc.params.namespace, tc.params.serviceAccount, tc.params.registry)(tc.args.ctx, tc.args.fn, tc.args.r)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nWithKubernetesAuthentication(...): -want error, +got error:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.r, tc.args.r, protocmp.Transform()); diff != "" {
-				t.Errorf("\n%s\nWithKubernetesAuthentication(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestRunFunctionPipeline(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type params struct {
-		c ContainerFunctionRunner
-	}
-
-	type args struct {
-		ctx context.Context
-		req CompositionRequest
-		s   *PTFCompositionState
-		o   iov1alpha1.Observed
-		d   iov1alpha1.Desired
-	}
-
-	type want struct {
-		s   *PTFCompositionState
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"UnsupportedFunctionTypeError": {
-			reason: "We should return an error if asked to run an unsupported function type.",
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Functions: []v1.Function{
-								{
-									Name: "cool-fn",
-									Type: v1.FunctionType("wat"),
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrapf(errors.Errorf(errFmtUnsupportedFnType, "wat"), errFmtRunFn, "cool-fn"),
-			},
-		},
-		"RunContainerFunctionError": {
-			reason: "We should return an error if we can't run a containerized function.",
-			params: params{
-				c: ContainerFunctionRunnerFn(func(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error) {
-					return nil, errBoom
-				}),
-			},
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Functions: []v1.Function{
-								{
-									Name: "cool-fn",
-									Type: v1.FunctionTypeContainer,
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrapf(errBoom, errFmtRunFn, "cool-fn"),
-			},
-		},
-		"ResultError": {
-			reason: "We should return the first result of Error severity.",
-			params: params{
-				c: ContainerFunctionRunnerFn(func(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error) {
-					return &iov1alpha1.FunctionIO{
-						Results: []iov1alpha1.Result{
-							{
-								Severity: iov1alpha1.SeverityFatal,
-								Message:  errBoom.Error(),
-							},
-						},
-					}, nil
-				}),
-			},
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Functions: []v1.Function{
-								{
-									Name: "cool-fn",
-									Type: v1.FunctionTypeContainer,
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrap(errBoom, errFatalResult),
-			},
-		},
-		"ParseCompositeError": {
-			reason: "We should return an error if we can't unmarshal the desired XR.",
-			params: params{
-				c: ContainerFunctionRunnerFn(func(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error) {
-					return &iov1alpha1.FunctionIO{
-						Desired: iov1alpha1.Desired{
-							Composite: iov1alpha1.DesiredComposite{
-								Resource: runtime.RawExtension{
-									// This triggers the unmarshal error.
-									Raw: []byte("}"),
-								},
-							},
-						},
-					}, nil
-				}),
-			},
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Functions: []v1.Function{
-								{
-									Name: "cool-fn",
-									Type: v1.FunctionTypeContainer,
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrap(json.Unmarshal([]byte("}"), nil), errUnmarshalDesiredXR),
-			},
-		},
-		"ParseComposedError": {
-			reason: "We should return an error if we can't unmarshal a desired composed resource.",
-			params: params{
-				c: ContainerFunctionRunnerFn(func(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error) {
-					return &iov1alpha1.FunctionIO{
-						Desired: iov1alpha1.Desired{
-							Composite: iov1alpha1.DesiredComposite{
-								Resource: runtime.RawExtension{
-									Raw: []byte(`{"apiVersion":"a/v1","kind":"XR"}`),
-								},
-							},
-							Resources: []iov1alpha1.DesiredResource{
-								{
-									Name: "cool-resource",
-									Resource: runtime.RawExtension{
-										// This triggers the unmarshal error.
-										Raw: []byte("}"),
-									},
-								},
-							},
-						},
-					}, nil
-				}),
-			},
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Functions: []v1.Function{
-								{
-									Name: "cool-fn",
-									Type: v1.FunctionTypeContainer,
-								},
-							},
-						},
-					},
-				},
-				s: &PTFCompositionState{},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					Composite: func() *composite.Unstructured {
-						xr := composite.New()
-						xr.SetAPIVersion("a/v1")
-						xr.SetKind("XR")
-						return xr
-					}(),
-				},
-				err: errors.Wrapf(errors.Wrap(json.Unmarshal([]byte("}"), nil), errUnmarshalDesiredCD), errFmtParseDesiredCD, "cool-resource"),
-			},
-		},
-		"Success": {
-			reason: "We should update our CompositionState with the results of the pipeline.",
-			params: params{
-				c: ContainerFunctionRunnerFn(func(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction, o ...ContainerFunctionRunnerOption) (*iov1alpha1.FunctionIO, error) {
-					return &iov1alpha1.FunctionIO{
-						Desired: iov1alpha1.Desired{
-							Composite: iov1alpha1.DesiredComposite{
-								Resource: runtime.RawExtension{
-									Raw: []byte(`{"apiVersion":"a/v1","kind":"XR"}`),
-								},
-								ConnectionDetails: []iov1alpha1.ExplicitConnectionDetail{
-									{
-										Name:  "a",
-										Value: "b",
-									},
-								},
-							},
-							// Note we test ParseDesiredResource separately.
-							Resources: []iov1alpha1.DesiredResource{
-								{
-									Name: "cool-resource",
-									Resource: runtime.RawExtension{
-										Raw: []byte(`{"apiVersion":"a/v1","kind":"Composed"}`),
-									},
-								},
-							},
-						},
-						Results: []iov1alpha1.Result{
-							{
-								Severity: iov1alpha1.SeverityWarning,
-								Message:  "oh no",
-							},
-							{
-								Severity: iov1alpha1.SeverityNormal,
-								Message:  "good stuff",
-							},
-						},
-					}, nil
-				}),
-			},
-			args: args{
-				req: CompositionRequest{
-					Revision: &v1.CompositionRevision{
-						Spec: v1.CompositionRevisionSpec{
-							Functions: []v1.Function{
-								{
-									Name: "cool-fn",
-									Type: v1.FunctionTypeContainer,
-								},
-							},
-						},
-					},
-				},
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					Composite: func() *composite.Unstructured {
-						xr := composite.New()
-						xr.SetAPIVersion("a/v1")
-						xr.SetKind("XR")
-						return xr
-					}(),
-					ConnectionDetails: managed.ConnectionDetails{"a": []byte("b")},
-					ComposedResources: ComposedResourceStates{
-						// We don't need to test that ParseDesiredResource works
-						// here - we do that elsewhere - so just call it.
-						"cool-resource": func() ComposedResourceState {
-							dr := iov1alpha1.DesiredResource{
-								Name: "cool-resource",
-								Resource: runtime.RawExtension{
-									Raw: []byte(`{"apiVersion":"a/v1","kind":"Composed"}`),
-								},
-							}
-							xr := composite.New()
-							xr.SetAPIVersion("a/v1")
-							xr.SetKind("XR")
-							cd, _ := ParseDesiredResource(dr, xr)
-							return cd
-						}(),
-					},
-					Events: []event.Event{
-						event.Warning(reasonCompose, errors.New("oh no")),
-						event.Normal(reasonCompose, "good stuff"),
-					},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			r := NewFunctionPipeline(tc.params.c)
-			err := r.RunFunctionPipeline(tc.args.ctx, tc.args.req, tc.args.s, tc.args.o, tc.args.d)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nRunFunctionPipeline(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.s, tc.args.s, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nRunFunctionPipeline(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
-
-type MockFunctionServer struct {
-	fnpbv1alpha1.UnimplementedContainerizedFunctionRunnerServiceServer
-
-	rsp *fnpbv1alpha1.RunFunctionResponse
-	err error
-}
-
-func (s *MockFunctionServer) RunFunction(context.Context, *fnpbv1alpha1.RunFunctionRequest) (*fnpbv1alpha1.RunFunctionResponse, error) {
-	return s.rsp, s.err
-}
-
-func TestRunFunction(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	fnio := &iov1alpha1.FunctionIO{
-		Desired: iov1alpha1.Desired{
-			Resources: []iov1alpha1.DesiredResource{
-				{Name: "cool-resource"},
-			},
-		},
-	}
-	fnioyaml, _ := yaml.Marshal(fnio)
-
-	type params struct {
-		server fnpbv1alpha1.ContainerizedFunctionRunnerServiceServer
-	}
-
-	type args struct {
-		ctx  context.Context
-		fnio *iov1alpha1.FunctionIO
-		fn   *v1.ContainerFunction
-	}
-
-	type want struct {
-		fnio *iov1alpha1.FunctionIO
-		err  error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"RunFunctionError": {
-			reason: "We should return an error if we can't make an RPC call to run the function.",
-			params: params{
-				server: &MockFunctionServer{err: errBoom},
-			},
-			args: args{
-				ctx:  context.Background(),
-				fnio: &iov1alpha1.FunctionIO{},
-				fn:   &v1.ContainerFunction{},
-			},
-			want: want{
-				err: errors.Wrap(status.Errorf(codes.Unknown, errBoom.Error()), errRunFnContainer),
-			},
-		},
-		"RunFunctionSuccess": {
-			reason: "We should return the same FunctionIO our server returned.",
-			params: params{
-				server: &MockFunctionServer{
-					rsp: &fnpbv1alpha1.RunFunctionResponse{
-						Output: fnioyaml,
-					},
-				},
-			},
-			args: args{
-				ctx:  context.Background(),
-				fnio: &iov1alpha1.FunctionIO{},
-				fn:   &v1.ContainerFunction{},
-			},
-			want: want{
-				fnio: fnio,
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			// Listen on a random port.
-			lis, err := net.Listen("tcp", "127.0.0.1:0")
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			wg := &sync.WaitGroup{}
-			wg.Add(1)
-			go func() {
-				s := grpc.NewServer()
-				fnpbv1alpha1.RegisterContainerizedFunctionRunnerServiceServer(s, tc.params.server)
-				_ = s.Serve(lis)
-				wg.Done()
-			}()
-
-			// Tell the function to connect to our mock server.
-			tc.args.fn.Runner = &v1.ContainerFunctionRunner{
-				Endpoint: pointer.String(lis.Addr().String()),
-			}
-
-			fnio, err := RunFunction(tc.args.ctx, tc.args.fnio, tc.args.fn)
-
-			_ = lis.Close() // This should terminate the goroutine above.
-			wg.Wait()
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nRunFunction(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.fnio, fnio); diff != "" {
-				t.Errorf("\n%s\nRunFunction(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
-
-func TestParseDesiredResource(t *testing.T) {
-
-	cd := composed.New()
-	cd.SetAPIVersion("a/v1")
-	cd.SetKind("Composed")
-	cd.SetName("composed")
-
-	desired := iov1alpha1.DesiredResource{
-		Name: "cool-resource",
-		Resource: runtime.RawExtension{
-			Raw: func() []byte {
-				j, _ := json.Marshal(cd)
-				return j
-			}(),
-		},
-	}
-
-	type args struct {
-		dr    iov1alpha1.DesiredResource
-		owner resource.Object
-	}
-
-	type want struct {
-		cd  ComposedResourceState
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		args   args
-		want   want
-	}{
-		"UnmarshalResourceError": {
-			reason: "We should return an error if we can't unmarshal our composed resource.",
-			args: args{
-				dr: iov1alpha1.DesiredResource{
-					Resource: runtime.RawExtension{
-						Raw: []byte("}"),
-					},
-				},
-			},
-			want: want{
-				err: errors.Wrap(json.Unmarshal([]byte("}"), nil), errUnmarshalDesiredCD),
-			},
-		},
-		"SetControllerRefError": {
-			reason: "We should return an error if we can't set a controller reference on our composed resource.",
-			args: args{
-				dr: iov1alpha1.DesiredResource{
-					Resource: runtime.RawExtension{
-						Raw: func() []byte {
-							u := cd.DeepCopy()
-							cd := &composed.Unstructured{Unstructured: *u}
-							meta.AddOwnerReference(cd, meta.AsController(&xpv1.TypedReference{
-								UID:  types.UID("someone-else"),
-								Kind: "Composite",
-								Name: "someone-else",
-							}))
-							j, _ := json.Marshal(cd)
-							return j
-						}(),
-					},
-				},
-				owner: &fake.Composite{},
-			},
-			want: want{
-				err: errors.Wrap(errors.New("composed is already controlled by Composite someone-else (UID someone-else)"), errSetControllerRef),
-			},
-		},
-		"Success": {
-			reason: "We should successfully translate a desired resource into composed resource state.",
-			args: args{
-				dr: desired,
-				owner: &fake.Composite{
-					ObjectMeta: metav1.ObjectMeta{
-						UID: types.UID("owner"),
-						Labels: map[string]string{
-							xcrd.LabelKeyNamePrefixForComposed: "pfx",
-							xcrd.LabelKeyClaimName:             "cool-claim",
-							xcrd.LabelKeyClaimNamespace:        "default",
-						},
-					},
-				},
-			},
-			want: want{
-				cd: ComposedResourceState{
-					ComposedResource: ComposedResource{
-						ResourceName: "cool-resource",
-					},
-					Desired: &desired,
-					Resource: func() *composed.Unstructured {
-						u := cd.DeepCopy()
-						cd := &composed.Unstructured{Unstructured: *u}
-						SetCompositionResourceName(cd, "cool-resource")
-						cd.SetOwnerReferences([]metav1.OwnerReference{{
-							UID:                types.UID("owner"),
-							Controller:         pointer.Bool(true),
-							BlockOwnerDeletion: pointer.Bool(true),
-						}})
-						cd.SetLabels(map[string]string{
-							xcrd.LabelKeyNamePrefixForComposed: "pfx",
-							xcrd.LabelKeyClaimName:             "cool-claim",
-							xcrd.LabelKeyClaimNamespace:        "default",
-						})
-						return cd
-					}(),
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			cd, err := ParseDesiredResource(tc.args.dr, tc.args.owner)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nParseDesiredResource(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.cd, cd); diff != "" {
-				t.Errorf("\n%s\nParseDesiredResource(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
-
-func TestImagePullConfig(t *testing.T) {
-
-	always := corev1.PullAlways
-	never := corev1.PullNever
-	ifNotPresent := corev1.PullIfNotPresent
-
-	cases := map[string]struct {
-		reason string
-		fn     *v1.ContainerFunction
-		want   *fnpbv1alpha1.ImagePullConfig
-	}{
-		"NoImagePullPolicy": {
-			reason: "We should return an empty config if there's no ImagePullPolicy.",
-			fn:     &v1.ContainerFunction{},
-			want:   &fnpbv1alpha1.ImagePullConfig{},
-		},
-		"PullAlways": {
-			reason: "We should correctly map PullAlways.",
-			fn: &v1.ContainerFunction{
-				ImagePullPolicy: &always,
-			},
-			want: &fnpbv1alpha1.ImagePullConfig{
-				PullPolicy: fnpbv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_ALWAYS,
-			},
-		},
-		"PullNever": {
-			reason: "We should correctly map PullNever.",
-			fn: &v1.ContainerFunction{
-				ImagePullPolicy: &never,
-			},
-			want: &fnpbv1alpha1.ImagePullConfig{
-				PullPolicy: fnpbv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_NEVER,
-			},
-		},
-		"PullIfNotPresent": {
-			reason: "We should correctly map PullIfNotPresent.",
-			fn: &v1.ContainerFunction{
-				ImagePullPolicy: &ifNotPresent,
-			},
-			want: &fnpbv1alpha1.ImagePullConfig{
-				PullPolicy: fnpbv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_IF_NOT_PRESENT,
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			got := ImagePullConfig(tc.fn)
-
-			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
-				t.Errorf("\n%s\nImagePullConfig(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestRunFunctionConfig(t *testing.T) {
-
-	cpu := computeresource.MustParse("1")
-	mem := computeresource.MustParse("256Mi")
-
-	isolated := v1.ContainerFunctionNetworkPolicyIsolated
-	runner := v1.ContainerFunctionNetworkPolicyRunner
-
-	cases := map[string]struct {
-		reason string
-		fn     *v1.ContainerFunction
-		want   *fnpbv1alpha1.RunFunctionConfig
-	}{
-		"EmptyConfig": {
-			reason: "An empty config should be returned when there is no run-related configuration.",
-			fn:     &v1.ContainerFunction{},
-			want:   &fnpbv1alpha1.RunFunctionConfig{},
-		},
-		"Resources": {
-			reason: "All resource quantities should be included in the RunFunctionConfig",
-			fn: &v1.ContainerFunction{
-				Resources: &v1.ContainerFunctionResources{
-					Limits: &v1.ContainerFunctionResourceLimits{
-						CPU:    &cpu,
-						Memory: &mem,
-					},
-				},
-			},
-			want: &fnpbv1alpha1.RunFunctionConfig{
-				Resources: &fnpbv1alpha1.ResourceConfig{
-					Limits: &fnpbv1alpha1.ResourceLimits{
-						Cpu:    cpu.String(),
-						Memory: mem.String(),
-					},
-				},
-			},
-		},
-		"IsolatedNetwork": {
-			reason: "The isolated network policy should be returned.",
-			fn: &v1.ContainerFunction{
-				Network: &v1.ContainerFunctionNetwork{
-					Policy: &isolated,
-				},
-			},
-			want: &fnpbv1alpha1.RunFunctionConfig{
-				Network: &fnpbv1alpha1.NetworkConfig{
-					Policy: fnpbv1alpha1.NetworkPolicy_NETWORK_POLICY_ISOLATED,
-				},
-			},
-		},
-		"RunnerNetwork": {
-			reason: "The runner network policy should be returned.",
-			fn: &v1.ContainerFunction{
-				Network: &v1.ContainerFunctionNetwork{
-					Policy: &runner,
-				},
-			},
-			want: &fnpbv1alpha1.RunFunctionConfig{
-				Network: &fnpbv1alpha1.NetworkConfig{
-					Policy: fnpbv1alpha1.NetworkPolicy_NETWORK_POLICY_RUNNER,
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			got := RunFunctionConfig(tc.fn)
-
-			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
-				t.Errorf("\n%s\nRunFunctionConfig(...): -want, +got:\n%s", tc.reason, diff)
-			}
-		})
-	}
-}
-
-func TestDeleteComposedResources(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type params struct {
-		client client.Writer
-	}
-
-	type args struct {
-		ctx context.Context
-		s   *PTFCompositionState
-	}
-
-	type want struct {
-		s   *PTFCompositionState
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"HasDesired": {
-			reason: "Desired resources should not be deleted.",
-			args: args{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"desired-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "desired-resource",
-							},
-							Desired: &iov1alpha1.DesiredResource{},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"desired-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "desired-resource",
-							},
-							Desired: &iov1alpha1.DesiredResource{},
-						},
-					},
-				},
-			},
-		},
-		"NeverCreated": {
-			reason: "Resources that were never created should be deleted from state, but not the API server.",
-			params: params{
-				client: &test.MockClient{
-					// We know Delete wasn't called because it's a nil function
-					// and would thus panic if it was.
-				},
-			},
-			args: args{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"undesired-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{},
-							Resource:         &fake.Composed{},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{},
-				},
-			},
-		},
-		"UncontrolledResource": {
-			reason: "Resources the XR doesn't control should be deleted from state, but not the API server.",
-			params: params{
-				client: &test.MockClient{
-					// We know Delete wasn't called because it's a nil function
-					// and would thus panic if it was.
-				},
-			},
-			args: args{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: "cool-xr",
-						},
-					},
-					ComposedResources: ComposedResourceStates{
-						"undesired-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{},
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									// This resource exists in the API server.
-									CreationTimestamp: metav1.Now(),
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: "cool-xr",
-						},
-					},
-					ComposedResources: ComposedResourceStates{},
-				},
-			},
-		},
-		"DeleteError": {
-			reason: "We should return any error encountered deleting the resource.",
-			params: params{
-				client: &test.MockClient{
-					MockDelete: test.NewMockDeleteFn(errBoom),
-				},
-			},
-			args: args{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: "cool-xr",
-						},
-					},
-					ComposedResources: ComposedResourceStates{
-						"undesired-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "undesired-resource",
-							},
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									// This resource exists in the API server.
-									CreationTimestamp: metav1.Now(),
-
-									// This resource is controlled by the XR.
-									OwnerReferences: []metav1.OwnerReference{{
-										Controller: pointer.Bool(true),
-										UID:        "cool-xr",
-									}},
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: "cool-xr",
-						},
-					},
-					ComposedResources: ComposedResourceStates{},
-				},
-				err: errors.Wrapf(errBoom, errFmtDeleteCD, "undesired-resource", "", ""),
-			},
-		},
-		"Success": {
-			reason: "We should successfully delete the resource from the API server and state.",
-			params: params{
-				client: &test.MockClient{
-					MockDelete: test.NewMockDeleteFn(nil),
-				},
-			},
-			args: args{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: "cool-xr",
-						},
-					},
-					ComposedResources: ComposedResourceStates{
-						"undesired-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "undesired-resource",
-							},
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									// This resource exists in the API server.
-									CreationTimestamp: metav1.Now(),
-
-									// This resource is controlled by the XR.
-									OwnerReferences: []metav1.OwnerReference{{
-										Controller: pointer.Bool(true),
-										UID:        "cool-xr",
-									}},
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ObjectMeta: metav1.ObjectMeta{
-							UID: "cool-xr",
-						},
-					},
-					ComposedResources: ComposedResourceStates{},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			d := NewUndesiredComposedResourceDeleter(tc.params.client)
-			err := d.DeleteComposedResources(tc.args.ctx, tc.args.s)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nDeleteComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.s, tc.args.s); diff != "" {
-				t.Errorf("\n%s\nDeleteComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
-
-func TestUpdateResourceRefs(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type args struct {
-		s *PTFCompositionState
-	}
-
-	type want struct {
-		s *PTFCompositionState
-	}
-
-	cases := map[string]struct {
-		reason string
-		args   args
-		want   want
-	}{
-		"RenderError": {
-			reason: "We shouldn't record references to resources that were never created and failed to render.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{},
-					ComposedResources: ComposedResourceStates{
-						"never-created": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-42",
-								},
-							},
-							TemplateRenderErr: errBoom,
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-							Refs: []corev1.ObjectReference{},
-						},
-					},
-					ComposedResources: ComposedResourceStates{
-						"never-created": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-42",
-								},
-							},
-							TemplateRenderErr: errBoom,
-						},
-					},
-				},
-			},
-		},
-		"Success": {
-			reason: "We should return a consistently ordered set of references.",
-			args: args{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{},
-					ComposedResources: ComposedResourceStates{
-						"never-created-c": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-c-42",
-								},
-							},
-						},
-						"never-created-b": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-b-42",
-								},
-							},
-						},
-						"never-created-a": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-a-42",
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					Composite: &fake.Composite{
-						ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
-							Refs: []corev1.ObjectReference{
-								{Name: "never-created-a-42"},
-								{Name: "never-created-b-42"},
-								{Name: "never-created-c-42"},
-							},
-						},
-					},
-					ComposedResources: ComposedResourceStates{
-						"never-created-c": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-c-42",
-								},
-							},
-						},
-						"never-created-b": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-b-42",
-								},
-							},
-						},
-						"never-created-a": ComposedResourceState{
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "never-created-a-42",
-								},
-							},
-						},
-					},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			UpdateResourceRefs(tc.args.s)
-
-			// We need to EquateErrors here for the TemplateRenderError in state.
-			if diff := cmp.Diff(tc.want.s, tc.args.s, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nUpdateResourceRefs(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
-
-func TestReadinessObserver(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type params struct {
-		c ReadinessChecker
-	}
-
-	type args struct {
-		ctx context.Context
-		s   *PTFCompositionState
-	}
-
-	type want struct {
-		s   *PTFCompositionState
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"IsReadyError": {
-			reason: "We should return any error encountered checking readiness.",
-			params: params{
-				c: ReadinessCheckerFn(func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
-					return false, errBoom
-				}),
-			},
-			args: args{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "cool-resource-42",
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "cool-resource-42",
-								},
-							},
-						},
-					},
-				},
-				err: errors.Wrapf(errBoom, errFmtReadiness, "cool-resource", "", "cool-resource-42"),
-			},
-		},
-		"Ready": {
-			reason: "We should record whether the composed resource is ready.",
-			params: params{
-				c: ReadinessCheckerFn(func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
-					return true, nil
-				}),
-			},
-			args: args{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: &fake.Composed{},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-								Ready:        true,
-							},
-							Resource: &fake.Composed{},
-						},
-					},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			o := NewReadinessObserver(tc.params.c)
-			err := o.ObserveComposedResources(tc.args.ctx, tc.args.s)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nObserveComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.s, tc.args.s, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nObserveComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
-
-func TestConnectionDetailsObserver(t *testing.T) {
-	errBoom := errors.New("boom")
-
-	type params struct {
-		e ConnectionDetailsExtractor
-	}
-
-	type args struct {
-		ctx context.Context
-		s   *PTFCompositionState
-	}
-
-	type want struct {
-		s   *PTFCompositionState
-		err error
-	}
-
-	cases := map[string]struct {
-		reason string
-		params params
-		args   args
-		want   want
-	}{
-		"ExtractConnectionError": {
-			reason: "We should return any error encountered extracting connetion details.",
-			params: params{
-				e: ConnectionDetailsExtractorFn(func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
-					return nil, errBoom
-				}),
-			},
-			args: args{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "cool-resource-42",
-								},
-							},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: &fake.Composed{
-								ObjectMeta: metav1.ObjectMeta{
-									Name: "cool-resource-42",
-								},
-							},
-						},
-					},
-				},
-				err: errors.Wrapf(errBoom, errFmtExtractConnectionDetails, "cool-resource", "", "cool-resource-42"),
-			},
-		},
-		"Success": {
-			reason: "We should record the extracted connection details.",
-			params: params{
-				e: ConnectionDetailsExtractorFn(func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
-					return managed.ConnectionDetails{"a": []byte("b")}, nil
-				}),
-			},
-			args: args{
-				s: &PTFCompositionState{
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: &fake.Composed{},
-						},
-					},
-				},
-			},
-			want: want{
-				s: &PTFCompositionState{
-					ConnectionDetails: managed.ConnectionDetails{"a": []byte("b")},
-					ComposedResources: ComposedResourceStates{
-						"cool-resource": ComposedResourceState{
-							ComposedResource: ComposedResource{
-								ResourceName: "cool-resource",
-							},
-							Resource: &fake.Composed{},
-						},
-					},
-				},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-
-			o := NewConnectionDetailsObserver(tc.params.e)
-			err := o.ObserveComposedResources(tc.args.ctx, tc.args.s)
-
-			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
-				t.Errorf("\n%s\nObserveComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-			if diff := cmp.Diff(tc.want.s, tc.args.s, cmpopts.EquateEmpty()); diff != "" {
-				t.Errorf("\n%s\nObserveComposedResources(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
diff --git a/internal/controller/apiextensions/composite/composition_render_test.go b/internal/controller/apiextensions/composite/composition_render_test.go
new file mode 100644
index 000000000..1be65ccd3
--- /dev/null
+++ b/internal/controller/apiextensions/composite/composition_render_test.go
@@ -0,0 +1,386 @@
+/*
+Copyright 2022 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+*/
+
+package composite
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/utils/pointer"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/errors"
+	"github.com/crossplane/crossplane-runtime/pkg/meta"
+	"github.com/crossplane/crossplane-runtime/pkg/resource"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
+	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+
+	"github.com/crossplane/crossplane/internal/xcrd"
+)
+
+func TestRenderFromJSON(t *testing.T) {
+	errInvalidChar := json.Unmarshal([]byte("olala"), &fake.Composed{})
+
+	type args struct {
+		o    resource.Object
+		data []byte
+	}
+	type want struct {
+		o   resource.Object
+		err error
+	}
+	cases := map[string]struct {
+		reason string
+		args
+		want
+	}{
+		"InvalidData": {
+			reason: "We should return an error if the data can't be unmarshalled",
+			args: args{
+				o:    &fake.Composed{},
+				data: []byte("olala"),
+			},
+			want: want{
+				o:   &fake.Composed{},
+				err: errors.Wrap(errInvalidChar, errUnmarshalJSON),
+			},
+		},
+		"ExistingGVKChanged": {
+			reason: "We should return an error if unmarshalling the base template changed the composed resource's group, version, or kind",
+			args: args{
+				o: composed.New(composed.FromReference(corev1.ObjectReference{
+					APIVersion: "example.org/v1",
+					Kind:       "Potato",
+				})),
+				data: []byte(`{"apiVersion": "example.org/v1", "kind": "Different"}`),
+			},
+			want: want{
+				o: composed.New(composed.FromReference(corev1.ObjectReference{
+					APIVersion: "example.org/v1",
+					Kind:       "Different",
+				})),
+				err: errors.Errorf(errFmtKindChanged, "example.org/v1, Kind=Potato", "example.org/v1, Kind=Different"),
+			},
+		},
+		"NewComposedResource": {
+			reason: "A valid base template should apply successfully to a new (empty) composed resource",
+			args: args{
+				o:    composed.New(),
+				data: []byte(`{"apiVersion": "example.org/v1", "kind": "Potato", "spec": {"cool": true}}`),
+			},
+			want: want{
+				o: &composed.Unstructured{Unstructured: unstructured.Unstructured{
+					Object: map[string]any{
+						"apiVersion": "example.org/v1",
+						"kind":       "Potato",
+						"spec": map[string]any{
+							"cool": true,
+						},
+					},
+				}},
+			},
+		},
+		"ExistingComposedResource": {
+			reason: "A valid base template should apply successfully to a new (empty) composed resource",
+			args: args{
+				o: composed.New(composed.FromReference(corev1.ObjectReference{
+					APIVersion: "example.org/v1",
+					Kind:       "Potato",
+					Name:       "ola-superrandom",
+				})),
+				data: []byte(`{"apiVersion": "example.org/v1", "kind": "Potato", "spec": {"cool": true}}`),
+			},
+			want: want{
+				o: &composed.Unstructured{Unstructured: unstructured.Unstructured{
+					Object: map[string]any{
+						"apiVersion": "example.org/v1",
+						"kind":       "Potato",
+						"metadata": map[string]any{
+							"name": "ola-superrandom",
+						},
+						"spec": map[string]any{
+							"cool": true,
+						},
+					},
+				}},
+			},
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			err := RenderFromJSON(tc.args.o, tc.args.data)
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nRenderFromJSON(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.o, tc.args.o); diff != "" {
+				t.Errorf("\n%s\nRenderFromJSON(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestRenderComposedResourceMetadata(t *testing.T) {
+	controlled := &fake.Composed{
+		ObjectMeta: metav1.ObjectMeta{
+			OwnerReferences: []metav1.OwnerReference{{
+				Controller: pointer.Bool(true),
+				UID:        "very-random",
+			}},
+		},
+	}
+	errRef := meta.AddControllerReference(controlled, metav1.OwnerReference{UID: "not-very-random"})
+
+	type args struct {
+		xr resource.Composite
+		cd resource.Composed
+		rn ResourceName
+	}
+	type want struct {
+		cd  resource.Composed
+		err error
+	}
+	cases := map[string]struct {
+		reason string
+		args
+		want
+	}{
+		"ConflictingControllerReference": {
+			reason: "We should return an error if the composed resource has an existing (and different) controller reference",
+			args: args{
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						UID: "somewhat-random",
+						Labels: map[string]string{
+							xcrd.LabelKeyNamePrefixForComposed: "prefix",
+							xcrd.LabelKeyClaimName:             "name",
+							xcrd.LabelKeyClaimNamespace:        "namespace",
+						},
+					},
+				},
+				cd: &fake.Composed{
+					ObjectMeta: metav1.ObjectMeta{
+						OwnerReferences: []metav1.OwnerReference{{
+							Controller: pointer.Bool(true),
+							UID:        "very-random",
+						}},
+					},
+				},
+			},
+			want: want{
+				cd: &fake.Composed{
+					ObjectMeta: metav1.ObjectMeta{
+						GenerateName: "prefix-",
+						OwnerReferences: []metav1.OwnerReference{{
+							Controller: pointer.Bool(true),
+							UID:        "very-random",
+						}},
+						Labels: map[string]string{
+							xcrd.LabelKeyNamePrefixForComposed: "prefix",
+							xcrd.LabelKeyClaimName:             "name",
+							xcrd.LabelKeyClaimNamespace:        "namespace",
+						},
+					},
+				},
+				err: errors.Wrap(errRef, errSetControllerRef),
+			},
+		},
+		"CompatibleControllerReference": {
+			reason: "We should not return an error if the composed resource has an existing (and matching) controller reference",
+			args: args{
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						UID: "somewhat-random",
+						Labels: map[string]string{
+							xcrd.LabelKeyNamePrefixForComposed: "prefix",
+							xcrd.LabelKeyClaimName:             "name",
+							xcrd.LabelKeyClaimNamespace:        "namespace",
+						},
+					},
+				},
+				cd: &fake.Composed{
+					ObjectMeta: metav1.ObjectMeta{
+						OwnerReferences: []metav1.OwnerReference{{
+							Controller: pointer.Bool(true),
+							UID:        "somewhat-random",
+						}},
+					},
+				},
+			},
+			want: want{
+				cd: &fake.Composed{
+					ObjectMeta: metav1.ObjectMeta{
+						GenerateName: "prefix-",
+						OwnerReferences: []metav1.OwnerReference{{
+							Controller:         pointer.Bool(true),
+							BlockOwnerDeletion: pointer.Bool(true),
+							UID:                "somewhat-random",
+						}},
+						Labels: map[string]string{
+							xcrd.LabelKeyNamePrefixForComposed: "prefix",
+							xcrd.LabelKeyClaimName:             "name",
+							xcrd.LabelKeyClaimNamespace:        "namespace",
+						},
+					},
+				},
+			},
+		},
+		"NoControllerReference": {
+			reason: "We should not return an error if the composed resource has no controller reference",
+			args: args{
+				xr: &fake.Composite{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "cool-xr",
+						UID:  "somewhat-random",
+						Labels: map[string]string{
+							xcrd.LabelKeyNamePrefixForComposed: "prefix",
+							xcrd.LabelKeyClaimName:             "name",
+							xcrd.LabelKeyClaimNamespace:        "namespace",
+						},
+					},
+				},
+				cd: &fake.Composed{},
+			},
+			want: want{
+				cd: &fake.Composed{
+					ObjectMeta: metav1.ObjectMeta{
+						GenerateName: "prefix-",
+						OwnerReferences: []metav1.OwnerReference{{
+							Controller:         pointer.Bool(true),
+							BlockOwnerDeletion: pointer.Bool(true),
+							UID:                "somewhat-random",
+							Name:               "cool-xr",
+						}},
+						Labels: map[string]string{
+							xcrd.LabelKeyNamePrefixForComposed: "prefix",
+							xcrd.LabelKeyClaimName:             "name",
+							xcrd.LabelKeyClaimNamespace:        "namespace",
+						},
+					},
+				},
+			},
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			err := RenderComposedResourceMetadata(tc.args.cd, tc.args.xr, tc.args.rn)
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nRenderComposedResourceMetadata(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.cd, tc.args.cd); diff != "" {
+				t.Errorf("\n%s\nRenderComposedResourceMetadata(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
+
+func TestDryRunRender(t *testing.T) {
+	errBoom := errors.New("boom")
+
+	type args struct {
+		ctx context.Context
+		cd  resource.Composed
+	}
+	type want struct {
+		cd  resource.Composed
+		err error
+	}
+	cases := map[string]struct {
+		reason string
+		client client.Client
+		args
+		want
+	}{
+		"SkipDryRunForNamedResources": {
+			reason: "We should not try to dry-run create resources that already have a name",
+			// We must be returning early, or else we'd hit this error.
+			client: &test.MockClient{MockCreate: test.NewMockCreateFn(errBoom)},
+			args: args{
+				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
+					Name: "already-has-a-cool-name",
+				}},
+			},
+			want: want{
+				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
+					Name: "already-has-a-cool-name",
+				}},
+				err: nil,
+			},
+		},
+		"SkipDryRunForResourcesWithoutGenerateName": {
+			reason: "We should not try to dry-run create resources that don't have a generate name (though that should never happen)",
+			// We must be returning early, or else we'd hit this error.
+			client: &test.MockClient{MockCreate: test.NewMockCreateFn(errBoom)},
+			args: args{
+				cd: &fake.Composed{}, // Conspicously missing a generate name.
+			},
+			want: want{
+				cd:  &fake.Composed{},
+				err: nil,
+			},
+		},
+		"DryRunError": {
+			reason: "Errors dry-run creating the rendered composed resource to name it should be returned",
+			client: &test.MockClient{MockCreate: test.NewMockCreateFn(errBoom)},
+			args: args{
+				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "cool-resource-",
+				}},
+			},
+			want: want{
+				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "cool-resource-",
+				}},
+				err: errors.Wrap(errBoom, errName),
+			},
+		},
+		"Success": {
+			reason: "Updates returned by dry-run creating the composed resource should be rendered",
+			client: &test.MockClient{MockCreate: test.NewMockCreateFn(nil, func(obj client.Object) error {
+				obj.SetName("cool-resource-42")
+				return nil
+			})},
+			args: args{
+				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "cool-resource-",
+				}},
+			},
+			want: want{
+				cd: &fake.Composed{ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "cool-resource-",
+					Name:         "cool-resource-42",
+				}},
+			},
+		},
+	}
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			r := NewAPIDryRunRenderer(tc.client)
+			err := r.DryRunRender(tc.args.ctx, tc.args.cd)
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nDryRunRender(...): -want, +got:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.cd, tc.args.cd); diff != "" {
+				t.Errorf("\n%s\nDryRunRender(...): -want, +got:\n%s", tc.reason, diff)
+			}
+		})
+	}
+}
diff --git a/internal/controller/apiextensions/composite/connection_test.go b/internal/controller/apiextensions/composite/connection_test.go
index eea508dc9..3424d51bb 100644
--- a/internal/controller/apiextensions/composite/connection_test.go
+++ b/internal/controller/apiextensions/composite/connection_test.go
@@ -36,7 +36,6 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 
-	iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 )
 
@@ -387,8 +386,6 @@ func TestExtractConnectionDetails(t *testing.T) {
 	}
 }
 
-// TODO(negz): Implement me.
-
 func TestExtractConfigsFromTemplate(t *testing.T) {
 	tfk := v1.ConnectionDetailTypeFromConnectionSecretKey
 
@@ -454,7 +451,7 @@ func TestExtractConfigsFromTemplate(t *testing.T) {
 
 	for name, tc := range cases {
 		t.Run(name, func(t *testing.T) {
-			cfgs := ExtractConfigsFromTemplate(tc.args.t)
+			cfgs := ExtractConfigsFromComposedTemplate(tc.args.t)
 
 			if diff := cmp.Diff(tc.want.cfgs, cfgs); diff != "" {
 				t.Errorf("\n%s\nExtractConfigsFromTemplate(...): -want, +got:\n%s", tc.reason, diff)
@@ -464,80 +461,6 @@ func TestExtractConfigsFromTemplate(t *testing.T) {
 	}
 }
 
-func TestExtractConfigsFromDesired(t *testing.T) {
-	type args struct {
-		d *iov1alpha1.DesiredResource
-	}
-	type want struct {
-		cfgs []ConnectionDetailExtractConfig
-	}
-
-	cases := map[string]struct {
-		reason string
-		args   args
-		want   want
-	}{
-		"NilResource": {
-			reason: "A nil desired resource should result in a nil slice of extract configs.",
-			args: args{
-				d: nil,
-			},
-			want: want{
-				cfgs: nil,
-			},
-		},
-		"ExplicitName": {
-			reason: "When a template's connection details have an explicit name, we should use it.",
-			args: args{
-				d: &iov1alpha1.DesiredResource{
-					ConnectionDetails: []iov1alpha1.DerivedConnectionDetail{{
-						Name:                    pointer.String("cool-detail"),
-						Type:                    iov1alpha1.ConnectionDetailTypeFromConnectionSecretKey,
-						FromConnectionSecretKey: pointer.String("cool-key"),
-					}},
-				},
-			},
-			want: want{
-				cfgs: []ConnectionDetailExtractConfig{{
-					Name:                    "cool-detail",
-					Type:                    ConnectionDetailTypeFromConnectionSecretKey,
-					FromConnectionSecretKey: pointer.String("cool-key"),
-				}},
-			},
-		},
-		"InferredName": {
-			reason: "When a template's connection details does not have an explicit name and is of TypeFromConnectionSecretKey, we should infer the name from the connection secret key.",
-			args: args{
-
-				d: &iov1alpha1.DesiredResource{
-					ConnectionDetails: []iov1alpha1.DerivedConnectionDetail{{
-						Type:                    iov1alpha1.ConnectionDetailTypeFromConnectionSecretKey,
-						FromConnectionSecretKey: pointer.String("cool-key"),
-					}},
-				},
-			},
-			want: want{
-				cfgs: []ConnectionDetailExtractConfig{{
-					Name:                    "cool-key",
-					Type:                    ConnectionDetailTypeFromConnectionSecretKey,
-					FromConnectionSecretKey: pointer.String("cool-key"),
-				}},
-			},
-		},
-	}
-
-	for name, tc := range cases {
-		t.Run(name, func(t *testing.T) {
-			cfgs := ExtractConfigsFromDesired(tc.args.d)
-
-			if diff := cmp.Diff(tc.want.cfgs, cfgs); diff != "" {
-				t.Errorf("\n%s\nExtractConfigsFromDesired(...): -want, +got:\n%s", tc.reason, diff)
-			}
-
-		})
-	}
-}
-
 func TestConnectionDetailType(t *testing.T) {
 	fromVal := v1.ConnectionDetailTypeFromValue
 	name := "coolsecret"
diff --git a/internal/controller/apiextensions/composite/fuzz_test.go b/internal/controller/apiextensions/composite/fuzz_test.go
index 62b1c2c47..b5a7270d3 100644
--- a/internal/controller/apiextensions/composite/fuzz_test.go
+++ b/internal/controller/apiextensions/composite/fuzz_test.go
@@ -25,9 +25,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/yaml"
 
-	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
-	"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
 
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
@@ -158,31 +156,3 @@ func YamlToUnstructured(yamlStr string) (*unstructured.Unstructured, error) {
 	}
 	return &unstructured.Unstructured{Object: obj}, nil
 }
-
-func FuzzPTFComposer(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte, yamlData string) {
-		ff := fuzz.NewConsumer(data)
-		state := &PTFCompositionState{}
-		unstr, err := YamlToUnstructured(yamlData)
-		if err != nil {
-			return
-		}
-		if unstr.Object == nil {
-			return
-		}
-		state.Composite = &composite.Unstructured{Unstructured: *unstr}
-		cd := &managed.ConnectionDetails{}
-		ff.GenerateStruct(cd)
-		state.ConnectionDetails = *cd
-		_, err = FunctionIOObserved(state)
-		if err != nil {
-			return
-		}
-
-		_, err = FunctionIODesired(state)
-		if err != nil {
-			return
-		}
-		UpdateResourceRefs(state)
-	})
-}
diff --git a/internal/controller/apiextensions/composite/reconciler_test.go b/internal/controller/apiextensions/composite/reconciler_test.go
index 4cddfef2d..afe3f054a 100644
--- a/internal/controller/apiextensions/composite/reconciler_test.go
+++ b/internal/controller/apiextensions/composite/reconciler_test.go
@@ -44,6 +44,8 @@ import (
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 )
 
+var _ Composer = ComposerSelectorFn(func(cm *v1.CompositionMode) Composer { return nil })
+
 func TestReconcile(t *testing.T) {
 	errBoom := errors.New("boom")
 	cd := managed.ConnectionDetails{"a": []byte("b")}
diff --git a/internal/controller/pkg/revision/deployment_test.go b/internal/controller/pkg/revision/deployment_test.go
index 7892d2573..bbf418ee0 100644
--- a/internal/controller/pkg/revision/deployment_test.go
+++ b/internal/controller/pkg/revision/deployment_test.go
@@ -27,9 +27,10 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
-	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 )
 
 type deploymentModifier func(*appsv1.Deployment)
@@ -101,7 +102,7 @@ func service(provider *pkgmetav1.Provider, rev v1.PackageRevision) *corev1.Servi
 	}
 }
 
-func serviceFunction(function *pkgmetav1alpha1.Function, rev v1.PackageRevision) *corev1.Service {
+func serviceFunction(function *pkgmetav1beta1.Function, rev v1.PackageRevision) *corev1.Service {
 	return &corev1.Service{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      function.GetName(),
@@ -121,6 +122,7 @@ func serviceFunction(function *pkgmetav1alpha1.Function, rev v1.PackageRevision)
 					TargetPort: intstr.FromInt(9443),
 				},
 			},
+			ClusterIP: corev1.ClusterIPNone,
 		},
 	}
 }
@@ -273,7 +275,7 @@ func deploymentProvider(provider *pkgmetav1.Provider, revision string, img strin
 	return d
 }
 
-func deploymentFunction(function *pkgmetav1alpha1.Function, revision string, img string, modifiers ...deploymentModifier) *appsv1.Deployment {
+func deploymentFunction(function *pkgmetav1beta1.Function, revision string, img string, modifiers ...deploymentModifier) *appsv1.Deployment {
 	var (
 		replicas = int32(1)
 	)
@@ -625,8 +627,8 @@ func TestBuildProviderDeployment(t *testing.T) {
 
 func TestBuildFunctionDeployment(t *testing.T) {
 	type args struct {
-		function *pkgmetav1alpha1.Function
-		revision *v1alpha1.FunctionRevision
+		function *pkgmetav1beta1.Function
+		revision *v1beta1.FunctionRevision
 		cc       *v1alpha1.ControllerConfig
 	}
 	type want struct {
@@ -642,25 +644,25 @@ func TestBuildFunctionDeployment(t *testing.T) {
 	tlsServerSecretName := "server-secret-name"
 	tlsClientSecretName := "client-secret-name"
 
-	functionWithoutImage := &pkgmetav1alpha1.Function{
+	functionWithoutImage := &pkgmetav1beta1.Function{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "pkg",
 		},
-		Spec: pkgmetav1alpha1.FunctionSpec{
+		Spec: pkgmetav1beta1.FunctionSpec{
 			Image: nil,
 		},
 	}
 
-	functionWithImage := &pkgmetav1alpha1.Function{
+	functionWithImage := &pkgmetav1beta1.Function{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "pkg",
 		},
-		Spec: pkgmetav1alpha1.FunctionSpec{
+		Spec: pkgmetav1beta1.FunctionSpec{
 			Image: &img,
 		},
 	}
 
-	revisionWithoutCC := &v1alpha1.FunctionRevision{
+	revisionWithoutCC := &v1beta1.FunctionRevision{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "rev-123",
 			Labels: map[string]string{
@@ -676,7 +678,7 @@ func TestBuildFunctionDeployment(t *testing.T) {
 		},
 	}
 
-	revisionWithCC := &v1alpha1.FunctionRevision{
+	revisionWithCC := &v1beta1.FunctionRevision{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: "rev-123",
 			Labels: map[string]string{
diff --git a/internal/controller/pkg/revision/hook_test.go b/internal/controller/pkg/revision/hook_test.go
index 36f23b06f..bd23e59cc 100644
--- a/internal/controller/pkg/revision/hook_test.go
+++ b/internal/controller/pkg/revision/hook_test.go
@@ -33,9 +33,10 @@ import (
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
-	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 	v1 "github.com/crossplane/crossplane/apis/pkg/v1"
 	"github.com/crossplane/crossplane/apis/pkg/v1alpha1"
+	"github.com/crossplane/crossplane/apis/pkg/v1beta1"
 	"github.com/crossplane/crossplane/internal/initializer"
 )
 
@@ -359,27 +360,27 @@ func TestHookPre(t *testing.T) {
 			reason: "Should only update status if function revision is active.",
 			args: args{
 				hook: &FunctionHooks{},
-				pkg: &pkgmetav1alpha1.Function{
-					Spec: pkgmetav1alpha1.FunctionSpec{
-						MetaSpec: pkgmetav1alpha1.MetaSpec{
-							Crossplane: &pkgmetav1alpha1.CrossplaneConstraints{
+				pkg: &pkgmetav1beta1.Function{
+					Spec: pkgmetav1beta1.FunctionSpec{
+						MetaSpec: pkgmetav1beta1.MetaSpec{
+							Crossplane: &pkgmetav1beta1.CrossplaneConstraints{
 								Version: crossplane,
 							},
-							DependsOn: []pkgmetav1alpha1.Dependency{{
+							DependsOn: []pkgmetav1beta1.Dependency{{
 								Function: &functionDep,
 								Version:  versionDep,
 							}},
 						},
 					},
 				},
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState: v1.PackageRevisionActive,
 					},
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState: v1.PackageRevisionActive,
 					},
@@ -404,20 +405,20 @@ func TestHookPre(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{
-					Spec: pkgmetav1alpha1.FunctionSpec{
-						MetaSpec: pkgmetav1alpha1.MetaSpec{
-							Crossplane: &pkgmetav1alpha1.CrossplaneConstraints{
+				pkg: &pkgmetav1beta1.Function{
+					Spec: pkgmetav1beta1.FunctionSpec{
+						MetaSpec: pkgmetav1beta1.MetaSpec{
+							Crossplane: &pkgmetav1beta1.CrossplaneConstraints{
 								Version: crossplane,
 							},
-							DependsOn: []pkgmetav1alpha1.Dependency{{
+							DependsOn: []pkgmetav1beta1.Dependency{{
 								Function: &functionDep,
 								Version:  versionDep,
 							}},
 						},
 					},
 				},
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionInactive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -425,7 +426,7 @@ func TestHookPre(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionInactive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -452,20 +453,20 @@ func TestHookPre(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{
-					Spec: pkgmetav1alpha1.FunctionSpec{
-						MetaSpec: pkgmetav1alpha1.MetaSpec{
-							Crossplane: &pkgmetav1alpha1.CrossplaneConstraints{
+				pkg: &pkgmetav1beta1.Function{
+					Spec: pkgmetav1beta1.FunctionSpec{
+						MetaSpec: pkgmetav1beta1.MetaSpec{
+							Crossplane: &pkgmetav1beta1.CrossplaneConstraints{
 								Version: crossplane,
 							},
-							DependsOn: []pkgmetav1alpha1.Dependency{{
+							DependsOn: []pkgmetav1beta1.Dependency{{
 								Function: &functionDep,
 								Version:  versionDep,
 							}},
 						},
 					},
 				},
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionInactive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -473,7 +474,7 @@ func TestHookPre(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionInactive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -494,20 +495,20 @@ func TestHookPre(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{
-					Spec: pkgmetav1alpha1.FunctionSpec{
-						MetaSpec: pkgmetav1alpha1.MetaSpec{
-							Crossplane: &pkgmetav1alpha1.CrossplaneConstraints{
+				pkg: &pkgmetav1beta1.Function{
+					Spec: pkgmetav1beta1.FunctionSpec{
+						MetaSpec: pkgmetav1beta1.MetaSpec{
+							Crossplane: &pkgmetav1beta1.CrossplaneConstraints{
 								Version: crossplane,
 							},
-							DependsOn: []pkgmetav1alpha1.Dependency{{
+							DependsOn: []pkgmetav1beta1.Dependency{{
 								Provider: &functionDep,
 								Version:  versionDep,
 							}},
 						},
 					},
 				},
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionInactive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -515,7 +516,7 @@ func TestHookPre(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionInactive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -966,15 +967,15 @@ func TestHookPost(t *testing.T) {
 			reason: "Should do nothing if function revision is inactive.",
 			args: args{
 				hook: &FunctionHooks{},
-				pkg:  &pkgmetav1alpha1.Function{},
-				rev: &v1alpha1.FunctionRevision{
+				pkg:  &pkgmetav1beta1.Function{},
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState: v1.PackageRevisionInactive,
 					},
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState: v1.PackageRevisionInactive,
 					},
@@ -1015,15 +1016,15 @@ func TestHookPost(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{},
-				rev: &v1alpha1.FunctionRevision{
+				pkg: &pkgmetav1beta1.Function{},
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState: v1.PackageRevisionActive,
 					},
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState: v1.PackageRevisionActive,
 					},
@@ -1070,8 +1071,8 @@ func TestHookPost(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{},
-				rev: &v1alpha1.FunctionRevision{
+				pkg: &pkgmetav1beta1.Function{},
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionActive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -1079,7 +1080,7 @@ func TestHookPost(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionActive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -1113,8 +1114,8 @@ func TestHookPost(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{},
-				rev: &v1alpha1.FunctionRevision{
+				pkg: &pkgmetav1beta1.Function{},
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						ControllerConfigReference: &v1.ControllerConfigReference{
 							Name: "custom-config",
@@ -1124,7 +1125,7 @@ func TestHookPost(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState: v1.PackageRevisionActive,
 						ControllerConfigReference: &v1.ControllerConfigReference{
@@ -1187,8 +1188,8 @@ func TestHookPost(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{},
-				rev: &v1alpha1.FunctionRevision{
+				pkg: &pkgmetav1beta1.Function{},
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionActive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -1196,7 +1197,7 @@ func TestHookPost(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					Spec: v1.PackageRevisionSpec{
 						DesiredState:        v1.PackageRevisionActive,
 						TLSServerSecretName: &tlsServerSecret,
@@ -1259,8 +1260,8 @@ func TestHookPost(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{},
-				rev: &v1alpha1.FunctionRevision{
+				pkg: &pkgmetav1beta1.Function{},
+				rev: &v1beta1.FunctionRevision{
 					ObjectMeta: metav1.ObjectMeta{
 						Labels: map[string]string{
 							v1.LabelParentPackage: "my-function",
@@ -1273,7 +1274,7 @@ func TestHookPost(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					ObjectMeta: metav1.ObjectMeta{
 						Labels: map[string]string{
 							v1.LabelParentPackage: "my-function",
@@ -1283,7 +1284,7 @@ func TestHookPost(t *testing.T) {
 						DesiredState:        v1.PackageRevisionActive,
 						TLSServerSecretName: &tlsServerSecret,
 					},
-					Status: v1alpha1.FunctionRevisionStatus{
+					Status: v1beta1.FunctionRevisionStatus{
 						PackageRevisionStatus: v1.PackageRevisionStatus{},
 						Endpoint:              fmt.Sprintf(serviceEndpointFmt, "my-function", namespace, servicePort),
 					},
@@ -1336,8 +1337,8 @@ func TestHookPost(t *testing.T) {
 						},
 					},
 				},
-				pkg: &pkgmetav1alpha1.Function{},
-				rev: &v1alpha1.FunctionRevision{
+				pkg: &pkgmetav1beta1.Function{},
+				rev: &v1beta1.FunctionRevision{
 					ObjectMeta: metav1.ObjectMeta{
 						Labels: map[string]string{
 							v1.LabelParentPackage: "my-function",
@@ -1350,7 +1351,7 @@ func TestHookPost(t *testing.T) {
 				},
 			},
 			want: want{
-				rev: &v1alpha1.FunctionRevision{
+				rev: &v1beta1.FunctionRevision{
 					ObjectMeta: metav1.ObjectMeta{
 						Labels: map[string]string{
 							v1.LabelParentPackage: "my-function",
@@ -1360,7 +1361,7 @@ func TestHookPost(t *testing.T) {
 						DesiredState:        v1.PackageRevisionActive,
 						TLSServerSecretName: &tlsServerSecret,
 					},
-					Status: v1alpha1.FunctionRevisionStatus{
+					Status: v1beta1.FunctionRevisionStatus{
 						PackageRevisionStatus: v1.PackageRevisionStatus{},
 						Endpoint:              fmt.Sprintf(serviceEndpointFmt, "my-function", namespace, servicePort),
 					},
diff --git a/internal/xcrd/crd_test.go b/internal/xcrd/crd_test.go
index a0218e446..51b35bb92 100644
--- a/internal/xcrd/crd_test.go
+++ b/internal/xcrd/crd_test.go
@@ -31,6 +31,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/pointer"
 
 	"github.com/crossplane/crossplane-runtime/pkg/errors"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
@@ -343,6 +344,7 @@ func TestForCompositeResource(t *testing.T) {
 															Required: []string{"apiVersion", "kind"},
 														},
 													},
+													XListType: pointer.String("atomic"),
 												},
 												"publishConnectionDetailsTo": {
 													Type:     "object",
@@ -605,6 +607,7 @@ func TestForCompositeResource(t *testing.T) {
 															Required: []string{"apiVersion", "kind"},
 														},
 													},
+													XListType: pointer.String("atomic"),
 												},
 												"publishConnectionDetailsTo": {
 													Type:     "object",
diff --git a/internal/xfn/function_runner_test.go b/internal/xfn/function_runner_test.go
new file mode 100644
index 000000000..4cc0eae36
--- /dev/null
+++ b/internal/xfn/function_runner_test.go
@@ -0,0 +1,394 @@
+/*
+Copyright 2023 The Crossplane Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed
+under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the License for the
+specific language governing permissions and limitations under the License.
+*/
+
+package xfn
+
+import (
+	"context"
+	"net"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/pkg/errors"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/protobuf/testing/protocmp"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+
+	"github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1beta1"
+	pkgv1 "github.com/crossplane/crossplane/apis/pkg/v1"
+	pkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
+)
+
+func TestRunFunction(t *testing.T) {
+	errBoom := errors.New("boom")
+
+	// Make sure to add servers listeners here, for us to later close.
+	listeners := make([]net.Listener, 0)
+
+	type params struct {
+		c client.Client
+		o []PackagedFunctionRunnerOption
+	}
+	type args struct {
+		ctx  context.Context
+		name string
+		req  *v1beta1.RunFunctionRequest
+	}
+	type want struct {
+		rsp *v1beta1.RunFunctionResponse
+		err error
+	}
+	cases := map[string]struct {
+		reason string
+		params params
+		args   args
+		want   want
+	}{
+		"ListFunctionRevisionError": {
+			reason: "We should return an error if we can't get (or verify) a client connection because we can't list FunctionRevisions",
+			params: params{
+				c: &test.MockClient{
+					MockList: test.NewMockListFn(errBoom),
+				},
+			},
+			args: args{
+				ctx:  context.Background(),
+				name: "cool-fn",
+			},
+			want: want{
+				err: errors.Wrapf(errors.Wrap(errBoom, errListFunctionRevisions), errFmtGetClientConn, "cool-fn"),
+			},
+		},
+		"NoActiveRevisions": {
+			reason: "We should return an error if we can't get (or verify) a client connection because no FunctionRevision is active",
+			params: params{
+				c: &test.MockClient{
+					MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error {
+						obj.(*pkgv1beta1.FunctionRevisionList).Items = []pkgv1beta1.FunctionRevision{
+							{
+								Spec: pkgv1.PackageRevisionSpec{
+									DesiredState: pkgv1.PackageRevisionInactive, // This revision is not active.
+								},
+							},
+						}
+						return nil
+					}),
+				},
+			},
+			args: args{
+				ctx:  context.Background(),
+				name: "cool-fn",
+			},
+			want: want{
+				err: errors.Wrapf(errors.New(errNoActiveRevisions), errFmtGetClientConn, "cool-fn"),
+			},
+		},
+		"ActiveRevisionHasNoEndpoint": {
+			reason: "We should return an error if we can't get (or verify) a client connection because the active FunctionRevision has an empty status.endpoint",
+			params: params{
+				c: &test.MockClient{
+					MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error {
+						obj.(*pkgv1beta1.FunctionRevisionList).Items = []pkgv1beta1.FunctionRevision{
+							{
+								ObjectMeta: metav1.ObjectMeta{
+									Name: "cool-fn-revision-a",
+								},
+								Spec: pkgv1.PackageRevisionSpec{
+									DesiredState: pkgv1.PackageRevisionActive,
+								},
+								Status: pkgv1beta1.FunctionRevisionStatus{
+									Endpoint: "", // An empty endpoint.
+								},
+							},
+						}
+						return nil
+					}),
+				},
+			},
+			args: args{
+				ctx:  context.Background(),
+				name: "cool-fn",
+			},
+			want: want{
+				err: errors.Wrapf(errors.Errorf(errFmtEmptyEndpoint, "cool-fn-revision-a"), errFmtGetClientConn, "cool-fn"),
+			},
+		},
+		"SuccessfulRequest": {
+			reason: "We should create a new client connection and successfully make a request if no client already exists",
+			params: params{
+				c: &test.MockClient{
+					MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error {
+						// Start a gRPC server.
+						lis := NewGRPCServer(t, &MockFunctionServer{rsp: &v1beta1.RunFunctionResponse{
+							Meta: &v1beta1.ResponseMeta{Tag: "hi!"},
+						}})
+						listeners = append(listeners, lis)
+
+						l, ok := obj.(*pkgv1beta1.FunctionRevisionList)
+						if !ok {
+							// If we're called to list Functions we want to
+							// return none, to make sure we GC everything.
+							return nil
+						}
+						l.Items = []pkgv1beta1.FunctionRevision{
+							{
+								ObjectMeta: metav1.ObjectMeta{
+									Name: "cool-fn-revision-a",
+								},
+								Spec: pkgv1.PackageRevisionSpec{
+									DesiredState: pkgv1.PackageRevisionActive,
+								},
+								Status: pkgv1beta1.FunctionRevisionStatus{
+									Endpoint: strings.Replace(lis.Addr().String(), "127.0.0.1", "dns:///localhost", 1),
+								},
+							},
+						}
+						return nil
+					}),
+				},
+			},
+			args: args{
+				ctx:  context.Background(),
+				name: "cool-fn",
+				req:  &v1beta1.RunFunctionRequest{},
+			},
+			want: want{
+				rsp: &v1beta1.RunFunctionResponse{
+					Meta: &v1beta1.ResponseMeta{Tag: "hi!"},
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			r := NewPackagedFunctionRunner(tc.params.c, tc.params.o...)
+			rsp, err := r.RunFunction(tc.args.ctx, tc.args.name, tc.args.req)
+
+			if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
+				t.Errorf("\n%s\nr.RunFunction(...): -want, +got:\n%s", tc.reason, diff)
+			}
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("\n%s\nr.RunFunction(...): -want error, +got error:\n%s", tc.reason, diff)
+			}
+
+			// Close any gRPC clients.
+			if _, err := r.GarbageCollectConnectionsNow(context.Background()); err != nil {
+				t.Logf("Error closing client connections: %s", err)
+			}
+		})
+	}
+
+	// Closing these listeners will close any gRPC servers.
+	for _, lis := range listeners {
+		if err := lis.Close(); err != nil {
+			t.Logf("Error closing server listener: %s", err)
+		}
+	}
+}
+
+func TestGetClientConn(t *testing.T) {
+	// TestRunFunction exercises most of the getClientConn code. Here we just
+	// test some cases that don't fit well in our usual table-driven format.
+
+	// Start a gRPC server.
+	lis := NewGRPCServer(t, &MockFunctionServer{rsp: &v1beta1.RunFunctionResponse{
+		Meta: &v1beta1.ResponseMeta{Tag: "hi!"},
+	}})
+	defer lis.Close()
+
+	target := strings.Replace(lis.Addr().String(), "127.0.0.1", "dns:///localhost", 1)
+
+	c := &test.MockClient{
+		MockList: NewListFn(target),
+	}
+
+	r := NewPackagedFunctionRunner(c)
+
+	// We should be able to create a new connection.
+	t.Run("CreateNewConnection", func(t *testing.T) {
+		conn, err := r.getClientConn(context.Background(), "cool-fn")
+
+		if diff := cmp.Diff(target, conn.Target()); diff != "" {
+			t.Errorf("\nr.getClientConn(...): -want, +got:\n%s", diff)
+		}
+		if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" {
+			t.Errorf("\nr.getClientConn(...): -want error, +got error:\n%s", diff)
+		}
+	})
+
+	// If we're called again and our FunctionRevision's endpoint hasn't changed,
+	// we should return our cached connection.
+	t.Run("ReuseExistingConnection", func(t *testing.T) {
+		conn, err := r.getClientConn(context.Background(), "cool-fn")
+
+		if diff := cmp.Diff(target, conn.Target()); diff != "" {
+			t.Errorf("\nr.getClientConn(...): -want, +got:\n%s", diff)
+		}
+		if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" {
+			t.Errorf("\nr.getClientConn(...): -want error, +got error:\n%s", diff)
+		}
+	})
+
+	// Start another gRPC server.
+	lis2 := NewGRPCServer(t, &MockFunctionServer{rsp: &v1beta1.RunFunctionResponse{
+		Meta: &v1beta1.ResponseMeta{Tag: "hi!"},
+	}})
+	defer lis2.Close()
+
+	target = strings.Replace(lis2.Addr().String(), "127.0.0.1", "dns:///localhost", 1)
+	c.MockList = NewListFn(target)
+
+	// If we're called again and our FunctionRevision's endpoint _has_ changed,
+	// we should close our cached connection and create a new one.
+	t.Run("ReplaceExistingConnection", func(t *testing.T) {
+		conn, err := r.getClientConn(context.Background(), "cool-fn")
+
+		if diff := cmp.Diff(target, conn.Target()); diff != "" {
+			t.Errorf("\nr.getClientConn(...): -want, +got:\n%s", diff)
+		}
+		if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" {
+			t.Errorf("\nr.getClientConn(...): -want error, +got error:\n%s", diff)
+		}
+	})
+
+	// Close any gRPC clients.
+	if _, err := r.GarbageCollectConnectionsNow(context.Background()); err != nil {
+		t.Logf("Error closing client connections: %s", err)
+	}
+}
+
+func TestGarbageCollectConnectionsNow(t *testing.T) {
+	// TestRunFunction exercises most of the GarbageCollectConnectionsNow code.
+	// Here we just test some cases that don't fit well in our usual
+	// table-driven format.
+
+	// Start a gRPC server.
+	lis := NewGRPCServer(t, &MockFunctionServer{rsp: &v1beta1.RunFunctionResponse{
+		Meta: &v1beta1.ResponseMeta{Tag: "hi!"},
+	}})
+	defer lis.Close()
+
+	target := strings.Replace(lis.Addr().String(), "127.0.0.1", "dns:///localhost", 1)
+
+	ctx := context.Background()
+	conn, err := grpc.DialContext(ctx, target, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		t.Fatalf("gRPC dial failed: %s", err)
+	}
+
+	c := &test.MockClient{}
+	r := NewPackagedFunctionRunner(c)
+
+	// Add our connection to our pool.
+	r.connsMx.Lock()
+	r.conns["cool-fn"] = conn
+	r.connsMx.Unlock()
+
+	t.Run("FunctionStillExistsDoNotGarbageCollect", func(t *testing.T) {
+		c.MockList = test.NewMockListFn(nil, func(obj client.ObjectList) error {
+			obj.(*pkgv1beta1.FunctionList).Items = []pkgv1beta1.Function{
+				{
+					// This Function exists!
+					ObjectMeta: metav1.ObjectMeta{Name: "cool-fn"},
+				},
+			}
+			return nil
+		})
+
+		i, err := r.GarbageCollectConnectionsNow(ctx)
+
+		if diff := cmp.Diff(0, i); diff != "" {
+			t.Errorf("\nr.GarbageCollectConnectionsNow(...): -want, +got:\n%s", diff)
+		}
+		if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" {
+			t.Errorf("\nr.GarbageCollectConnectionsNow(...): -want error, +got error:\n%s", diff)
+		}
+	})
+
+	t.Run("FunctionDoesNotExistsGarbageCollect", func(t *testing.T) {
+		// No Functions exist
+		c.MockList = test.NewMockListFn(nil)
+
+		i, err := r.GarbageCollectConnectionsNow(ctx)
+
+		if diff := cmp.Diff(1, i); diff != "" {
+			t.Errorf("\nr.GarbageCollectConnectionsNow(...): -want, +got:\n%s", diff)
+		}
+		if diff := cmp.Diff(nil, err, test.EquateErrors()); diff != "" {
+			t.Errorf("\nr.GarbageCollectConnectionsNow(...): -want error, +got error:\n%s", diff)
+		}
+	})
+}
+
+func NewListFn(target string) test.MockListFn {
+	return test.NewMockListFn(nil, func(obj client.ObjectList) error {
+		l, ok := obj.(*pkgv1beta1.FunctionRevisionList)
+		if !ok {
+			// If we're called to list Functions we want to
+			// return none, to make sure we GC everything.
+			return nil
+		}
+		l.Items = []pkgv1beta1.FunctionRevision{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "cool-fn-revision-a",
+				},
+				Spec: pkgv1.PackageRevisionSpec{
+					DesiredState: pkgv1.PackageRevisionActive,
+				},
+				Status: pkgv1beta1.FunctionRevisionStatus{
+					Endpoint: target,
+				},
+			},
+		}
+		return nil
+	})
+}
+
+func NewGRPCServer(t *testing.T, ss v1beta1.FunctionRunnerServiceServer) net.Listener {
+	// Listen on a random port.
+	lis, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Logf("Listening for gRPC connections on %q", lis.Addr().String())
+
+	// TODO(negz): Is it worth using a WaitGroup for these?
+	go func() {
+		s := grpc.NewServer()
+		v1beta1.RegisterFunctionRunnerServiceServer(s, ss)
+		_ = s.Serve(lis)
+	}()
+
+	// The caller must close this listener to terminate the server.
+	return lis
+}
+
+type MockFunctionServer struct {
+	v1beta1.UnimplementedFunctionRunnerServiceServer
+
+	rsp *v1beta1.RunFunctionResponse
+	err error
+}
+
+func (s *MockFunctionServer) RunFunction(context.Context, *v1beta1.RunFunctionRequest) (*v1beta1.RunFunctionResponse, error) {
+	return s.rsp, s.err
+}
diff --git a/internal/xpkg/lint_test.go b/internal/xpkg/lint_test.go
index a25741b1f..a1786ee69 100644
--- a/internal/xpkg/lint_test.go
+++ b/internal/xpkg/lint_test.go
@@ -35,6 +35,7 @@ import (
 	v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
 	pkgmetav1 "github.com/crossplane/crossplane/apis/pkg/meta/v1"
 	pkgmetav1alpha1 "github.com/crossplane/crossplane/apis/pkg/meta/v1alpha1"
+	pkgmetav1beta1 "github.com/crossplane/crossplane/apis/pkg/meta/v1beta1"
 	"github.com/crossplane/crossplane/internal/version"
 	"github.com/crossplane/crossplane/internal/version/fake"
 )
@@ -93,7 +94,7 @@ metadata:
 	_                = yaml.Unmarshal(v1alpha1ProvBytes, v1alpha1ProvMeta)
 	v1alpha1ConfMeta = &pkgmetav1alpha1.Configuration{}
 	_                = yaml.Unmarshal(v1alpha1ConfBytes, v1alpha1ConfMeta)
-	v1alpha1FuncMeta = &pkgmetav1alpha1.Function{}
+	v1alpha1FuncMeta = &pkgmetav1beta1.Function{}
 	_                = yaml.Unmarshal(v1alpha1FuncBytes, v1alpha1FuncMeta)
 	v1ProvMeta       = &pkgmetav1.Provider{}
 	_                = yaml.Unmarshal(v1ProvBytes, v1ProvMeta)
diff --git a/test/e2e/apiextensions_test.go b/test/e2e/apiextensions_test.go
index 3e6ec40db..c5841e761 100644
--- a/test/e2e/apiextensions_test.go
+++ b/test/e2e/apiextensions_test.go
@@ -103,5 +103,42 @@ func TestCompositionPatchAndTransform(t *testing.T) {
 			)).
 			Feature(),
 	)
+}
+
+// TODO(negz): How do we want to handle beta features? They're on by default.
+// Maybe in this case add a test suite that tests P&T when Functions are
+// disabled?
 
+func TestCompositionFunctions(t *testing.T) {
+
+	manifests := "test/e2e/manifests/apiextensions/composition/functions"
+	environment.Test(t,
+		features.New(t.Name()).
+			WithLabel(LabelArea, LabelAreaAPIExtensions).
+			WithLabel(LabelSize, LabelSizeSmall).
+			WithLabel(config.LabelTestSuite, config.TestSuiteDefault).
+			WithSetup("CreatePrerequisites", funcs.AllOf(
+				funcs.ApplyResources(FieldManager, manifests, "setup/*.yaml"),
+				funcs.ResourcesCreatedWithin(30*time.Second, manifests, "setup/*.yaml"),
+				funcs.ResourcesHaveConditionWithin(1*time.Minute, manifests, "setup/definition.yaml", apiextensionsv1.WatchingComposite()),
+			)).
+			Assess("CreateClaim", funcs.AllOf(
+				funcs.ApplyResources(FieldManager, manifests, "claim.yaml"),
+				funcs.ResourcesCreatedWithin(30*time.Second, manifests, "claim.yaml"),
+			)).
+			Assess("ClaimIsReady",
+				funcs.ResourcesHaveConditionWithin(5*time.Minute, manifests, "claim.yaml", xpv1.Available())).
+			Assess("ClaimHasPatchedField",
+				funcs.ResourcesHaveFieldValueWithin(5*time.Minute, manifests, "claim.yaml", "status.coolerField", "I'M COOLER!"),
+			).
+			WithTeardown("DeleteClaim", funcs.AllOf(
+				funcs.DeleteResources(manifests, "claim.yaml"),
+				funcs.ResourcesDeletedWithin(2*time.Minute, manifests, "claim.yaml"),
+			)).
+			WithTeardown("DeletePrerequisites", funcs.AllOf(
+				funcs.DeleteResources(manifests, "setup/*.yaml"),
+				funcs.ResourcesDeletedWithin(3*time.Minute, manifests, "setup/*.yaml"),
+			)).
+			Feature(),
+	)
 }
diff --git a/test/e2e/manifests/apiextensions/composition/functions/claim.yaml b/test/e2e/manifests/apiextensions/composition/functions/claim.yaml
new file mode 100644
index 000000000..a0fa6f904
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/functions/claim.yaml
@@ -0,0 +1,10 @@
+apiVersion: nop.example.org/v1alpha1
+kind: NopResource
+metadata:
+  namespace: default
+  name: apiextensions-composition-functions
+spec:
+  coolField: "I'm cool!"
+  # This is necessary to ensure the claim's MRs are actually gone before we
+  # delete the Provider - https://github.com/crossplane/crossplane/issues/4251
+  compositeDeletePolicy: Foreground
diff --git a/test/e2e/manifests/apiextensions/composition/functions/setup/composition.yaml b/test/e2e/manifests/apiextensions/composition/functions/setup/composition.yaml
new file mode 100644
index 000000000..abc4817b5
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/functions/setup/composition.yaml
@@ -0,0 +1,42 @@
+apiVersion: apiextensions.crossplane.io/v1
+kind: Composition
+metadata:
+  name: xnopresources.nop.example.org
+spec:
+  compositeTypeRef:
+    apiVersion: nop.example.org/v1alpha1
+    kind: XNopResource
+  mode: Pipeline
+  pipeline:
+  - step: be-a-dummy
+    functionRef:
+      name: function-dummy
+    input:
+      apiVersion: dummy.fn.crossplane.io/v1beta1
+      kind: Response
+      # This is a YAML-serialized RunFunctionResponse. function-dummy will
+      # overlay the desired state on any that was passed into it.
+      response:
+        desired:
+          composite:
+            resource:
+              status:
+                coolerField: "I'M COOLER!"
+          resources:
+            nop-resource-1:
+              resource:
+                apiVersion: nop.crossplane.io/v1alpha1
+                kind: NopResource
+                spec:
+                  forProvider:
+                    conditionAfter:
+                    - conditionType: Ready
+                      conditionStatus: "False"
+                      time: 0s
+                    - conditionType: Ready
+                      conditionStatus: "True"
+                      time: 10s
+              ready: READY_TRUE
+        results:
+         - severity: SEVERITY_NORMAL
+           message: "I am doing a compose!"
diff --git a/test/e2e/manifests/apiextensions/composition/functions/setup/definition.yaml b/test/e2e/manifests/apiextensions/composition/functions/setup/definition.yaml
new file mode 100644
index 000000000..afe55de66
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/functions/setup/definition.yaml
@@ -0,0 +1,34 @@
+apiVersion: apiextensions.crossplane.io/v1
+kind: CompositeResourceDefinition
+metadata:
+  name: xnopresources.nop.example.org
+spec:
+  group: nop.example.org
+  names:
+    kind: XNopResource
+    plural: xnopresources
+  claimNames:
+    kind: NopResource
+    plural: nopresources
+  connectionSecretKeys:
+  - test
+  versions:
+  - name: v1alpha1
+    served: true
+    referenceable: true
+    schema:
+     openAPIV3Schema:
+       type: object
+       properties:
+        spec:
+          type: object
+          properties:
+            coolField:
+              type: string
+          required:
+          - coolField
+        status:
+          type: object
+          properties:
+            coolerField:
+              type: string
\ No newline at end of file
diff --git a/test/e2e/manifests/apiextensions/composition/functions/setup/function.yaml b/test/e2e/manifests/apiextensions/composition/functions/setup/function.yaml
new file mode 100644
index 000000000..cb423f28e
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/functions/setup/function.yaml
@@ -0,0 +1,8 @@
+apiVersion: pkg.crossplane.io/v1beta1
+kind: Function
+metadata:
+  name: function-dummy
+spec:
+  # NOTE(negz): This is currently manually pushed. See README.md at
+  # https://github.com/crossplane-contrib/function-dummy.
+  package: xpkg.upbound.io/crossplane-contrib/function-dummy:v0.2.0
diff --git a/test/e2e/manifests/apiextensions/composition/functions/setup/provider.yaml b/test/e2e/manifests/apiextensions/composition/functions/setup/provider.yaml
new file mode 100644
index 000000000..2f8d0708f
--- /dev/null
+++ b/test/e2e/manifests/apiextensions/composition/functions/setup/provider.yaml
@@ -0,0 +1,7 @@
+apiVersion: pkg.crossplane.io/v1
+kind: Provider
+metadata:
+  name: provider-nop
+spec:
+  package: xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0
+  ignoreCrossplaneConstraints: true
\ No newline at end of file