Skip to main content
Every resource implements lifecycle methods that Pragmatiks calls when managing your resources. Handlers are observe-then-act: each resource owns exactly one external object, findable from its identity and config alone. on_observe reports whether that object exists and what it looks like; on_create, on_update, and on_delete converge it to the configuration.

Method Signatures

All lifecycle methods are async and run within the provider runtime. Do I/O through an async client or await asyncio.to_thread(...), so a handler yields to the event loop and can be cancelled. on_observe is mandatory unless the resource is computed. Registering a non-computed class without it raises MissingObserveError when the provider package is imported, so pragma providers publish fails naming the resource type.

Instance Attributes

Inside lifecycle methods, you have access to several instance attributes:

on_create

Called when a resource is first created. Your implementation should provision the underlying infrastructure and return the outputs.
When called: The first time a resource is applied. Return value: An instance of your Outputs class containing values that other resources can reference.

on_observe

Reports the external object this resource owns. Locate it solely from identity (self.project_id, self.name) and self.config, including resolved dependencies, never from self.outputs. Read only the external system, never other resources; a handler that needs another resource declares a Dependency.
Return value: None when no object exists for this identity. Otherwise the same Outputs the write path returned for that state. Errors: Let anything other than “not found” propagate. An exception means the observation failed, not that the object is absent. When the external system can hold more than one match for the key, raise on more than one match. Stable outputs: Every output field is derived from identity or config, or read from the object once the write path has waited for it to be ready, and on_observe reads it the same way. Values that change without a platform write, such as replica readiness, counters, or indexing status, belong in health(), never in outputs.

on_update

Called to converge an existing object to the current configuration. Receives the previous configuration so you can limit the work to what changed.
When called: Each time the resource configuration changes after initial creation, and when a create finds the object already present. Parameters:
  • previous_config: The configuration from the previous successful apply, or None when the object exists but the platform holds no prior configuration. On None, skip “unchanged, return early” shortcuts and apply the full desired state.
Return value: An instance of your Outputs class, read from the converged object.

Common Update Patterns

Immutable fields - Fields that cannot change in place, and every field on_observe reads to locate the object, are declared ImmutableField or ImmutableDependency. The platform refuses a change to them, so a user changes one by deleting and recreating the resource. The handler never checks them:
No-op when unchanged - Skip work when the live object already matches the configuration:

on_delete

Called when a resource is being removed. Clean up the underlying infrastructure.
When called: When a resource is removed from a graph or explicitly deleted. Return value: None. Any return value is ignored.

Idempotency Requirements

All lifecycle methods must be idempotent. The same method may be called multiple times with the same input, and must produce the same result. Creating an object that already exists converges it through on_update(previous_config=None); deleting an object that is already gone succeeds.
This happens when:
  • The runtime restarts after executing a method but before confirming completion
  • Network issues cause retries
  • The user re-applies the same configuration

Idempotent Create

Handle the case where a previous attempt already created the resource by reading it instead:

Idempotent Delete

Handle the case where the resource is already gone; that counts as success:

Idempotent Update

Compare the live object with the configuration and write only the difference. With previous_config=None, never assume nothing changed:

Error Handling

Raise exceptions to signal failures. The runtime will:
  1. Mark the resource as failed
  2. Record the error message
  3. Stop processing dependent resources
Use specific exception types:
  • ValueError - Invalid configuration (user error)
  • RuntimeError - Infrastructure or API failures
  • Let unexpected exceptions propagate with full tracebacks

Complete Example

Computed Resources

A resource with no external object of its own declares computed = True. It needs no on_observe: its outputs derive from its configuration, or from the subresources it owns. Composition resources that only assemble subresources are computed.

Creating Subresources

Providers can create and manage subresources from within lifecycle handlers. This enables composition patterns where a high-level resource provisions multiple lower-level resources.

The Pattern

Build the subresource instance, set this resource as its owner, and apply it:
The handler never waits for the subresource. apply() returns once the platform accepts the subresource, and the platform provisions it after the handler returns. A resource that needs the subresource’s outputs, such as its connection string, declares a Dependency or a field reference on it; the platform runs that resource only once the subresource is ready, with the values resolved. See Configuration and Outputs.

Methods

set_owner(parent) - Establishes an ownership relationship. When the parent resource is deleted, owned subresources are automatically cleaned up via cascading deletes. Returns self for method chaining. apply() - Sends the resource to the API for creation or update and returns once the API accepts it. The platform then provisions the resource with the provider that handles its type. Returns self for method chaining.
Never wait inside a handler for another resource to become ready: no wait_ready, no apply(wait=True), no sleep or retry loop. Declare the dependency and let the platform sequence the work.

Complete Example

Here’s a complete example of an application resource that provisions its own database, using the Database resource from the complete example above. The application owns no external object of its own, so it is computed, and its outputs derive from its own identity:

When to Use Subresources

Use subresources when the child resource’s lifecycle is tightly coupled to the parent. If users might want to manage the child independently, expose it as a separate resource type instead.