Idempotency
See Lifecycle Methods - Idempotency Requirements for patterns on implementing idempotent observe, create, update, and delete operations. Key principles:- Observe: Locate the object from identity and config alone; return
Nonewhen it does not exist - Create: Handle “already exists” errors by retrieving the existing resource
- Update: Converge to the full desired state; when
previous_configisNone, apply everything - Delete: Treat “not found” as success, not failure
Error Handling
Use Specific Exception Types
Choose exception types that communicate the nature of the failure:RuntimeError for infrastructure and API failures, ValueError for user configuration errors:
Let Unexpected Errors Propagate
Do not catch all exceptions. Handle only the errors you expect and let everything else propagate with its full traceback for debugging:except Exception and re-raise a generic RuntimeError: it hides the original failure behind a message that says nothing.
Provide Actionable Error Messages
Error messages should help users understand what went wrong and how to fix it. Say which field is wrong and what to change:"Invalid configuration" leaves the user guessing.
Credentials Management
Accept Credentials as Configuration
Providers should accept credentials as configuration, not rely on ambient credentials:- Works in multi-tenant environments where each user has different credentials
- Makes credential requirements explicit in the schema
- Enables users to manage credentials via Pragma secrets
Use FieldReference for Secrets
Document that users should useFieldReference to pass credentials from a secret resource:
Never Log Credentials
Ensure credentials never appear in logs or error messages. The authentication error from a client library can echo the credentials it was given, so raise a message of your own and drop the original withfrom None:
Performance
Cache Clients Within Lifecycle Methods
Create API clients once per lifecycle invocation, not per operation:Minimize API Calls in Updates
Only call external APIs for fields that changed.previous_config is None when the object exists but the platform holds no prior configuration; apply the full desired state then:
Use Async Where Possible
Lifecycle methods areasync. Use async API clients, or run a blocking call with await asyncio.to_thread(...), and run independent calls concurrently with asyncio.gather. A handler that never awaits cannot be cancelled:
Common Mistakes
Forgetting Idempotency
The most common mistake is assuming lifecycle methods only run once. Anon_create that calls create_secret unguarded fails on a retry, because the first attempt already created the secret. Handle “already exists” by reading the secret instead:
Mutable State Between Calls
Do not store state on the resource instance between lifecycle calls. Each call runs on a fresh instance, so a client thaton_create stored on self does not exist when on_update runs. Build what a handler needs inside that handler:
Checking Immutable Fields in Handlers
Fields that cannot change in place, and every fieldon_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. Never compare these fields against previous_config in a handler:
Reading self.outputs
self.outputs is never populated when a handler runs. Every handler locates the object from identity (self.name, self.project_id) and self.config, and reads its state from the external system:
health(), never in outputs.
Incomplete Cleanup in on_delete
Delete all resources created byon_create, not just the primary resource. Each step tolerates its object being gone, and the database goes last, because it is what locates the others: a delete interrupted halfway still finds the database on the next attempt and finishes the job:
What’s Next
Lifecycle Methods
Deep dive into on_observe, on_create, on_update, and on_delete.
SDK Reference
Full SDK documentation.