Skip to main content
Config and Outputs are Pydantic models that define your resource’s interface. Config specifies what users configure, while Outputs defines what your resource exposes to dependent resources.

Config Basics

Config classes inherit from Config, which extends Pydantic’s BaseModel with extra="forbid" to catch typos:
from pragma_sdk import Config

class DatabaseConfig(Config):
    name: str
    size_gb: int = 10
    region: str = "us-east-1"
Key behaviors:
  • Unknown fields raise ValidationError (catches typos in YAML)
  • Type coercion is automatic (string "10" becomes int 10)
  • All Pydantic validation features work (Field, validators, etc.)

Outputs Basics

Outputs classes inherit from Outputs with the same extra="forbid" setting:
from pragma_sdk import Outputs

class DatabaseOutputs(Outputs):
    database_id: str
    connection_url: str
    host: str
Outputs are returned from on_create and on_update lifecycle methods. Other resources can reference these fields through the dependency system.

Field Types

Required Fields

Fields without defaults are required:
class SecretConfig(Config):
    project_id: str       # Required
    secret_id: str        # Required
    data: str             # Required

Optional Fields with Defaults

Provide sensible defaults for common cases:
class DatabaseConfig(Config):
    name: str
    size_gb: int = 10           # Default: 10 GB
    region: str = "us-east-1"   # Default: US East
    backup_enabled: bool = True # Default: backups on

Optional Fields That May Be Absent

Use None default for truly optional fields:
class DatabaseConfig(Config):
    name: str
    description: str | None = None
    tags: list[str] | None = None

FieldReference for Dynamic Values

The Field type alias allows config fields to accept either a direct value or a reference to another resource’s output:
from pragma_sdk import Config, Field, FieldReference

class AppConfig(Config):
    name: str
    database_url: Field[str]  # Can be string OR FieldReference
Users can provide a direct value:
provider: mycompany
resource: app
name: my-app
config:
  name: my-app
  database_url: "postgres://localhost/mydb"
Or reference another resource’s output:
provider: mycompany
resource: app
name: my-app
config:
  name: my-app
  database_url:
    provider: mycompany
    resource: database
    name: shared-db
    field: connection_url

How FieldReference Works

When the runtime processes a resource, it resolves all FieldReference values before calling your lifecycle methods. By the time on_create runs, self.config.database_url contains the actual string value, not the reference.
You don’t need to handle FieldReference resolution in your code. The runtime resolves all references to their actual values before invoking lifecycle methods.

When to Use Field

Use Field[T] for config values that commonly come from other resources:
class AppConfig(Config):
    name: str
    # These might reference outputs from other resources
    database_url: Field[str]
    api_key: Field[str]

    # These are always user-provided
    port: int = 8080
    log_level: str = "INFO"

Dependency for Whole-Resource Access

When you need access to an entire resource (its config, outputs, and methods) rather than just a single field, use Dependency[T]:
from pragma_sdk import Config, Dependency

class AppConfig(Config):
    name: str
    database: Dependency[DatabaseResource]  # Access to full resource

Resolving Dependencies

In your lifecycle methods, call resolve() to get the typed resource instance:
async def on_create(self) -> AppOutputs:
    # Get the full database resource
    db = await self.config.database.resolve()

    # Access any field from config or outputs
    print(f"Connecting to {db.config.name}")
    print(f"URL: {db.outputs.connection_url}")

    return AppOutputs(db_url=db.outputs.connection_url)
The runtime resolves dependencies before calling your lifecycle handler. The resolve() method returns the pre-resolved instance. If the dependent resource is not yet READY, it will raise a RuntimeError.

YAML Syntax

Users specify whole-resource dependencies without a field key:
provider: mycompany
resource: app
name: my-app
config:
  name: my-app
  database:
    provider: mycompany
    resource: database
    name: shared-db
    # No "field" key - this is a whole-resource dependency

Dependency vs FieldReference

Use CaseTypeYAML
Need one output valueField[str]Include field key
Need multiple outputsDependency[T]Omit field key
Need to call resource methodsDependency[T]Omit field key
Simple string/int valueField[T]Include field key
Example comparison:
class AppConfig(Config):
    # FieldReference: just need the connection URL string
    database_url: Field[str]

    # Dependency: need multiple values or the full resource
    database: Dependency[DatabaseResource]
# FieldReference usage (gets single value)
database_url:
  provider: mycompany
  resource: database
  name: shared-db
  field: connection_url

# Dependency usage (gets full resource)
database:
  provider: mycompany
  resource: database
  name: shared-db

Validation Patterns

Field Constraints

Use Pydantic’s Field function for constraints:
from pydantic import Field as PydanticField
from pragma_sdk import Config

class DatabaseConfig(Config):
    name: str = PydanticField(min_length=3, max_length=63)
    size_gb: int = PydanticField(ge=10, le=10000, default=10)
    port: int = PydanticField(ge=1024, le=65535, default=5432)
Import Pydantic’s Field as PydanticField to avoid confusion with the SDK’s Field type alias used for FieldReference support.

Field Validators

Validate individual fields with custom logic:
from pydantic import field_validator
from pragma_sdk import Config

class BucketConfig(Config):
    name: str
    region: str

    @field_validator("name")
    @classmethod
    def validate_bucket_name(cls, v: str) -> str:
        if not v.islower():
            raise ValueError("Bucket name must be lowercase")
        if not v.replace("-", "").isalnum():
            raise ValueError("Bucket name can only contain letters, numbers, and hyphens")
        return v

    @field_validator("region")
    @classmethod
    def validate_region(cls, v: str) -> str:
        valid_regions = {"us-east-1", "us-west-2", "eu-west-1"}
        if v not in valid_regions:
            raise ValueError(f"Region must be one of: {valid_regions}")
        return v

Model Validators

Validate relationships between fields:
from pydantic import model_validator
from pragma_sdk import Config

class ReplicaConfig(Config):
    min_replicas: int = 1
    max_replicas: int = 10

    @model_validator(mode="after")
    def validate_replica_range(self) -> "ReplicaConfig":
        if self.min_replicas > self.max_replicas:
            raise ValueError("min_replicas cannot exceed max_replicas")
        return self

Type Coercion

Pydantic automatically coerces compatible types:
class DatabaseConfig(Config):
    name: str
    size_gb: int
    enabled: bool

# All of these work:
config = DatabaseConfig(name="db", size_gb="100", enabled="true")
config = DatabaseConfig(name="db", size_gb=100, enabled=1)
For strict typing without coercion, use strict mode on fields:
from pydantic import Field as PydanticField

class StrictConfig(Config):
    count: int = PydanticField(strict=True)  # Rejects string "10"

Output Design Best Practices

Expose What Dependents Need

Think about what downstream resources will need:
class DatabaseOutputs(Outputs):
    # Connection details for apps
    connection_url: str
    host: str
    port: int

    # Identity for management operations
    database_id: str

    # Resource path for cloud operations
    resource_name: str

Use Consistent Naming

Follow these conventions:
PatternUse ForExample
*_idUnique identifiersdatabase_id, cluster_id
*_nameResource names/pathsresource_name, bucket_name
*_urlConnection stringsconnection_url, endpoint_url
*_arn / *_uriCloud resource identifiersrole_arn, topic_uri

Keep Outputs Stable

Output field names are part of your API. Changing them breaks dependent resources:
# Good: Stable output names
class SecretOutputs(Outputs):
    resource_name: str    # Don't rename to secret_name later
    version_name: str
    version_id: str

# Avoid: Adding fields that might be removed
class SecretOutputs(Outputs):
    resource_name: str
    internal_state: dict  # Don't expose internal details

Include Sufficient Context

Provide enough information for dependents to work without additional API calls:
# Minimal: Dependents need to make additional calls
class SecretOutputs(Outputs):
    secret_id: str

# Better: Dependents have what they need
class SecretOutputs(Outputs):
    resource_name: str   # Full GCP resource path
    version_name: str    # Full version path
    version_id: str      # Just the version number

Complete Example

Here’s a well-designed Config and Outputs pair:
from pydantic import Field as PydanticField, field_validator, model_validator
from pragma_sdk import Config, Field, Outputs

class QueueConfig(Config):
    """Configuration for a message queue.

    Attributes:
        name: Queue identifier (lowercase, alphanumeric with hyphens).
        max_message_size_kb: Maximum message size (1-256 KB).
        retention_days: How long to retain messages (1-14 days).
        dead_letter_queue: Optional DLQ name for failed messages.
        encryption_key: KMS key for encryption (can reference a key resource).
    """
    name: str = PydanticField(min_length=3, max_length=80)
    max_message_size_kb: int = PydanticField(ge=1, le=256, default=64)
    retention_days: int = PydanticField(ge=1, le=14, default=4)
    dead_letter_queue: str | None = None
    encryption_key: Field[str] | None = None

    @field_validator("name")
    @classmethod
    def validate_queue_name(cls, v: str) -> str:
        if not v.replace("-", "").isalnum():
            raise ValueError("Queue name can only contain letters, numbers, and hyphens")
        return v.lower()

    @model_validator(mode="after")
    def validate_dlq_not_self(self) -> "QueueConfig":
        if self.dead_letter_queue == self.name:
            raise ValueError("Dead letter queue cannot reference itself")
        return self


class QueueOutputs(Outputs):
    """Outputs from queue creation.

    Attributes:
        queue_url: Full URL for sending messages.
        queue_arn: ARN for IAM policies.
        queue_name: Canonical queue name.
    """
    queue_url: str
    queue_arn: str
    queue_name: str

What’s Next