provider-resources
Enables implementing Terraform Provider resources and data sources using the Plugin Framework with complete CRUD operations, schema design, plan modifiers and…
Identity proven by GitHub's verified organisation badge (hashicorp)
- Agents
- Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode, claude.ai and API
- License
- MPL-2.0 (copyleft)
- Pin
- f706481pinned Oct 9, 2026
- Context cost
- about 4,400 tokenswhen the skill loads
- GitHub stars
- 890on hashicorp/agent-skills
- Scripts
- None found
What it does
“Implement Terraform Provider resources and data sources using the Plugin Framework: CRUD operations, schema design, plan modifiers and validators, not-found handling, waiters for eventually consistent APIs, import support, resource design principles, and required acceptance test coverage. Use when adding or changing a resource or data source, deciding whether an API concept should be a resource, wiring a resource to the provider's configured client, handling drift or resource-not-found, or reviewing a resource implementation before submission.”
Generated summary from the skill's own text
Enables implementing Terraform Provider resources and data sources using the Plugin Framework with complete CRUD operations, schema design, plan modifiers and validators, eventual consistency handling, and acceptance test patterns. Covers resource structure, state management, import support, and error handling for adding or modifying infrastructure resources.
When to use it
- Adding new resources or data sources to a Terraform provider
- Implementing CRUD operations and schema for infrastructure objects
- Handling eventual consistency and resource-not-found scenarios
- Reviewing resource implementations before submission to registry
When not to
- Migrating existing SDKv2 resources to Framework without behavior analysis
- Creating resources that span multiple services or API endpoints
- Implementing SDKv2 resources in existing SDKv2-only providers
Install
These commands install from the publisher's repository and need no account.
skills CLI
npx skills add hashicorp/agent-skills --skill provider-resourcesInstalls from the repository's default branch, which may have changed since the commit we checked.
GitHub CLI 2.90 or later
gh skill install hashicorp/agent-skills provider-resources@f706481af9b8fedb66de909f6243ad29601afa0cInstalls the commit we checked (f706481).
Copy by hand
Download the folder at f706481 and put it in .claude/skills/provider-resources for Claude Code and in .agents/skills/provider-resources for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.
Copy for your agent
A prompt for any coding agent. It installs this skill from GitHub at the pinned commit, copies the files as they are, and runs nothing unless you ask.
Prompt
Install the agent skill "provider-resources" by HashiCorp. Source, pinned to commit f706481af9b8fedb66de909f6243ad29601afa0c: https://github.com/hashicorp/agent-skills/tree/f706481af9b8fedb66de909f6243ad29601afa0c/plugins/terraform/skills/provider-resources 1. Download that folder at exactly this commit. Do not use the default branch. 2. Copy every file unchanged, keeping the folder structure, into .claude/skills/provider-resources/ for Claude Code and .agents/skills/provider-resources/ for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. Create the folders if they do not exist. 3. Do not edit the files, and do not run any script in the folder unless I ask you to. 4. When you are done, list the files you copied and where you put them. Listing and checks: https://www.agentnative.dev/skills/hashicorp/provider-resources
SKILL.md at the pin
---
name: provider-resources
description: >-
Implement Terraform Provider resources and data sources using the Plugin
Framework: CRUD operations, schema design, plan modifiers and validators,
not-found handling, waiters for eventually consistent APIs, import support,
resource design principles, and required acceptance test coverage. Use when
adding or changing a resource or data source, deciding whether an API
concept should be a resource, wiring a resource to the provider's
configured client, handling drift or resource-not-found, or reviewing a
resource implementation before submission.
license: MPL-2.0
metadata:
lifecycle-status: active
copyright: Copyright IBM Corp. 2026
version: "0.0.1"
---
# Terraform Provider Resources Implementation Guide
## Overview
This guide covers developing Terraform Provider resources and data sources.
Resources represent infrastructure objects that Terraform manages through
Create, Read, Update, and Delete (CRUD) operations.
**Use the [Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework)
for all net-new resources and data sources.** Plugin SDKv2 is for maintaining
resources that already exist on it; do not write new code against it. A
provider can serve both during migration by muxing
([terraform-plugin-mux](https://developer.hashicorp.com/terraform/plugin/mux)),
so adopting the Framework never requires a big-bang rewrite. To tell which
mode an existing provider is in, check `go.mod`: `terraform-plugin-mux`
present means it serves both SDKv2 and Framework code; only
`terraform-plugin-sdk/v2` means SDKv2-only; only
`terraform-plugin-framework` means Framework-only. Be cautious about
*migrating* existing SDKv2 resources: the Framework distinguishes null from
zero values, so naive migrations change behavior for existing users (use the
`provider-framework-migration` skill, if available).
**References** (load when needed):
- `references/design-principles.md` — what should (and should not) become a
resource; data source semantics; relationship and async-task modeling
- `references/retries-and-waiters.md` — eventual consistency, retry
patterns, and status/wait function structure
## File Structure
Most providers keep every resource in a single package:
```
internal/provider/
├── provider.go # Provider schema + Configure
├── widget_resource.go # Resource implementation
├── widget_resource_test.go # Acceptance tests
├── widget_data_source.go # Data source (if applicable)
└── widget_data_source_test.go
```
Large multi-service providers (e.g. terraform-provider-aws) split into
`internal/service/<service>/` packages instead, with an idiomatic file
taxonomy worth adopting once a package grows: `consts.go`, `find.go`
(finders), `status.go` (status functions), `wait.go` (waiters), `sweep.go`
(test sweepers), `exports_test.go`.
Documentation lives in `docs/` and is generated with `tfplugindocs`:
```
docs/
├── resources/<name>.md # generated; optional <name>.md.tmpl template
└── data-sources/<name>.md
```
(Hand-written `website/docs/r/*.html.markdown` trees exist in some older,
large providers — follow the target repo's convention when editing one.)
## Resource Structure
A Framework resource is a struct holding the API client, with interface
assertions making the implemented behaviors explicit:
```go
var (
_ resource.Resource = &widgetResource{}
_ resource.ResourceWithConfigure = &widgetResource{}
_ resource.ResourceWithImportState = &widgetResource{}
)
func NewWidgetResource() resource.Resource {
return &widgetResource{}
}
type widgetResource struct {
client *examplecloud.Client
}
func (r *widgetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_widget"
}
// Configure receives the client the provider built in its own Configure.
func (r *widgetResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return // provider not yet configured (e.g. validation phase)
}
client, ok := req.ProviderData.(*examplecloud.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *examplecloud.Client, got: %T.", req.ProviderData),
)
return
}
r.client = client
}
func (r *widgetResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthBetween(1, 255),
},
},
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
```
How the provider's `Configure` produces that client — schema, credential
resolution, validation — is covered by the `provider-configuration` skill
(if available).
**On `id`:** SDKv2 required a magic `id` attribute; the Framework does not.
If the API has its own identifier, expose it under its real meaning and do
not add a second, redundant `id`. Only keep `id` when it *is* the API's
identifier (as above).
## CRUD Operations
### Create
```go
func (r *widgetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data widgetResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
input := &examplecloud.CreateWidgetInput{
Name: data.Name.ValueStringPointer(),
}
output, err := r.client.CreateWidget(ctx, input)
if err != nil {
resp.Diagnostics.AddError(
"Error creating Widget",
fmt.Sprintf("creating Widget (%s): %s", data.Name.ValueString(), err),
)
return
}
data.ID = types.StringPointerValue(output.ID)
// For eventually consistent APIs, wait for the resource to be usable
// before returning — see references/retries-and-waiters.md.
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
```
### Read
Read must handle out-of-band deletion by removing the resource from state so
the next plan recreates it, rather than erroring forever:
```go
func (r *widgetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data widgetResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
output, err := findWidgetByID(ctx, r.client, data.ID.ValueString())
if isNotFound(err) {
tflog.Warn(ctx, "Widget not found, removing from state", map[string]any{"id": data.ID.ValueString()})
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError(
"Error reading Widget",
fmt.Sprintf("reading Widget (%s): %s", data.ID.ValueString(), err),
)
return
}
data.Name = types.StringPointerValue(output.Name)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
```
### Update
Only call the API for attributes that actually changed; compare plan against
state:
```go
func (r *widgetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state widgetResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if !plan.Description.Equal(state.Description) {
input := &examplecloud.UpdateWidgetInput{
ID: plan.ID.ValueStringPointer(),
Description: plan.Description.ValueStringPointer(),
}
if _, err := r.client.UpdateWidget(ctx, input); err != nil {
resp.Diagnostics.AddError(
"Error updating Widget",
fmt.Sprintf("updating Widget (%s): %s", plan.ID.ValueString(), err),
)
return
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
```
### Delete
Treat "already gone" as success — the desired end state is reached:
```go
func (r *widgetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data widgetResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
_, err := r.client.DeleteWidget(ctx, &examplecloud.DeleteWidgetInput{
ID: data.ID.ValueStringPointer(),
})
if isNotFound(err) {
return
}
if err != nil {
resp.Diagnostics.AddError(
"Error deleting Widget",
fmt.Sprintf("deleting Widget (%s): %s", data.ID.ValueString(), err),
)
return
}
}
```
### Import
With `ResourceWithImportState` asserted, passthrough of the identifier is
one line:
```go
func (r *widgetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
```
For multi-part identifiers, parse a delimited import ID (commonly
comma-separated) and set each attribute explicitly.
## Resource Design Principles
Before implementing, check the shape of the thing being modeled (full
treatment in `references/design-principles.md`):
- A resource is the *smallest* useful building block; if the API offers
CRUD for it, it likely deserves its own resource.
- A resource should talk to **one** API/service only — cross-service
resources break permissions, auditing, and endpoint configuration.
- Data sources are read-only and side-effect free. A *singular* data source
errors on zero or multiple matches; a *plural* data source (plural noun
name) returns zero-or-more as a collection and errors on neither.
- Attached policies/rules, long-running task invocations, and versioned
artifacts usually deserve their *own* resources rather than attributes on
the parent.
- Start/stop or enable/disable state belongs as an attribute *in* the
resource, not as a separate resource.
## Schema Design
### Attribute Types
| Terraform Type | Framework Type | Use Case |
|----------------|----------------|----------|
| `string` | `schema.StringAttribute` | Names, identifiers |
| `number` | `schema.Int64Attribute`, `schema.Float64Attribute` | Counts, sizes |
| `bool` | `schema.BoolAttribute` | Feature flags |
| `list` | `schema.ListAttribute` | Ordered collections |
| `set` | `schema.SetAttribute` | Unordered unique items |
| `map` | `schema.MapAttribute` | Key-value pairs |
| `object` | `schema.SingleNestedAttribute` | Complex nested config |
Give every attribute a `MarkdownDescription` — `tfplugindocs` publishes it,
and it is the primary user-facing documentation.
### Plan Modifiers
```go
// Force replacement when value changes
stringplanmodifier.RequiresReplace()
// Keep a known value during plan instead of (known after apply)
stringplanmodifier.UseStateForUnknown()
```
### Validators
```go
stringvalidator.LengthBetween(1, 255)
stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9-]+$`), "must be lowercase alphanumeric with hyphens")
stringvalidator.OneOf("small", "medium", "large")
int64validator.Between(1, 100)
listvalidator.SizeAtLeast(1)
```
### Sensitive Attributes
```go
"password": schema.StringAttribute{
Required: true,
Sensitive: true,
},
```
## State Management
### Finders
Centralize "get one thing or a typed not-found" in a finder so Read, Delete,
waiters, and tests all share identical not-found semantics:
```go
func findWidgetByID(ctx context.Context, client *examplecloud.Client, id string) (*examplecloud.Widget, error) {
output, err := client.GetWidget(ctx, &examplecloud.GetWidgetInput{ID: &id})
if err != nil {
var apiErr *examplecloud.NotFoundError
if errors.As(err, &apiErr) {
return nil, &retry.NotFoundError{LastError: err}
}
return nil, fmt.Errorf("getting Widget (%s): %w", id, err)
}
if output == nil || output.Widget == nil {
return nil, &retry.NotFoundError{Message: "empty result"}
}
return output.Widget, nil
}
func isNotFound(err error) bool {
var nfe *retry.NotFoundError
return errors.As(err, &nfe)
}
```
### Waiting for Resource States
Many APIs return from Create/Delete before the resource is usable/gone. Use
`retry.StateChangeConf` (from
`github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry` — usable from
Framework providers), with a status function built on the finder and
timeouts in named constants:
```go
stateConf := &retry.StateChangeConf{
Pending: []string{"CREATING", "PENDING"},
Target: []string{"ACTIVE"},
Refresh: statusWidget(ctx, r.client, id), // one poll of the finder: (obj, status, err)
Timeout: widgetCreatedTimeout,
}
outputRaw, err := stateConf.WaitForStateContext(ctx)
```
The full status/wait function pairs (create and delete waiters, failure-state
handling, post-create not-found retries, eventual-consistency patterns) are
in `references/retries-and-waiters.md` — read it whenever the API is
asynchronous or eventually consistent.
## Testing
Every resource ships with, at minimum:
- **`_basic`** — create with minimal config, assert attributes, then an
import step (`ImportState: true`, `ImportStateVerify: true`)
- **`_disappears`** — delete the object out-of-band mid-test; the next plan
must propose recreation, not error
- **Per-attribute tests** — exercise updates for each non-trivial argument
Naming grammar: tests `TestAcc{Resource}_{group?}_{description}`, helpers
`testAccCheck{Resource}Exists` / `testAccCheck{Resource}Destroy`, config
functions `testAcc{Resource}Config_{description}`. Keep configs
self-contained, randomize real resource names, and never hardcode
environment-specific values (account IDs, zones, versions).
```go
func TestAccWidget_basic(t *testing.T) {
rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
resourceName := "examplecloud_widget.test"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckWidgetDestroy,
Steps: []resource.TestStep{
{
Config: testAccWidgetConfig_basic(rName),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("name"), knownvalue.StringExact(rName)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("id"), knownvalue.NotNull()),
},
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
func testAccWidgetConfig_basic(rName string) string {
return fmt.Sprintf(`
resource "examplecloud_widget" "test" {
name = %[1]q
}
`, rName)
}
```
Use the `provider-test-patterns` skill (if available) for the full testing
treatment: config helper style (`%[1]q` indexed verbs), statecheck/plancheck,
CompareValue, custom StateCheck implementations for exists/disappears
helpers, sweepers, and ephemeral resource testing. Use the
`run-acceptance-tests` skill for executing and debugging test runs.
## Error Handling
Match API errors by type, not message text, and wrap with context:
```go
var notFound *examplecloud.NotFoundError
if errors.As(err, ¬Found) {
// resource doesn't exist
}
// Wrapping inside helpers: preserve the cause with %w
return fmt.Errorf("creating Widget (%s): %w", name, err)
```
Diagnostics follow a consistent grammar — summary names the operation and
type, detail carries identifier and cause:
```go
resp.Diagnostics.AddError(
"Error creating Widget",
fmt.Sprintf("creating Widget (%s): %s", name, err),
)
resp.Diagnostics.AddAttributeError(
path.Root("name"),
"Invalid name",
"Name must be lowercase alphanumeric",
)
```
## Documentation
Write attribute `MarkdownDescription`s first — they are the source of
truth. Then generate Registry documentation with `tfplugindocs`
(`go generate ./...` where wired up), adding `docs/**/*.md.tmpl` templates
only for prose and examples the generator cannot derive. Use the
`provider-docs` skill (if available) for the full documentation workflow and
Registry publication rules.
## Pre-Submission Checklist
- [ ] Plugin Framework used (no new SDKv2 code)
- [ ] Resource has all CRUD operations implemented
- [ ] Read removes missing resources from state; Delete tolerates already-deleted
- [ ] No redundant `id` attribute (real API identifier exposed instead)
- [ ] Import implemented and covered by an `ImportStateVerify` step
- [ ] `_basic`, `_disappears`, and per-attribute tests present
- [ ] Waiters used where the API is eventually consistent
- [ ] Error messages name the operation, type, and identifier
- [ ] Sensitive attributes marked; every attribute has a description
- [ ] Docs generated with `tfplugindocs`
- [ ] Changelog entry added, if the repo tracks release notes (check CONTRIBUTING)
## References
- [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework)
- [Resource Development](https://developer.hashicorp.com/terraform/plugin/framework/resources)
- [Data Source Development](https://developer.hashicorp.com/terraform/plugin/framework/data-sources)
- [HashiCorp Provider Design Principles](https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles)
From hashicorp/agent-skills at f706481, shown as plain text under its license: MPL-2.0 (copyleft). The license text is in plugins/terraform/skills/provider-resources/SKILL.md.
View SKILL.md on GitHub at f706481Files
- SKILL.md18 KB · 3f87a1c9e8b8
- references/
- design-principles.md5.0 KB · a94f21fa2038
- retries-and-waiters.md5.8 KB · 960edd4af491
3 files, 29 KB in total. Each file has a sha256 in the pin's manifest; the first 12 characters are shown.
Skill folder on GitHub at f706481More from HashiCorp
aws-ami-builder
This skill enables the agent to build custom Amazon Machine Images using Packer's amazon-ebs builder.
Claude Code, Codex, Cursor +4 · about 870 tokens · pinned 6 days ago
azure-image-builder
This skill uses Packer's Azure ARM builder to create custom managed images and Compute Gallery images in Azure.
Claude Code, Codex, Cursor +4 · about 1K tokens · pinned 6 days ago
azure-verified-modules
This skill provides comprehensive requirements and best practices for developing Azure Terraform modules that meet Azure Verified Modules (AVM) certification…
Claude Code, Codex, Cursor +4 · about 4K tokens · pinned 6 days ago
new-terraform-provider
Scaffolds a new Terraform provider using the Plugin Framework by creating workspace structure, initializing a Go module, and generating main.go and…
Contains scripts
Claude Code, Codex, Cursor +3 · about 330 tokens · pinned 6 days ago
provider-actions
This skill guides implementing Terraform Actions, imperative operations that execute at provider lifecycle events using the Plugin Framework.
Claude Code, Codex, Cursor +4 · about 4.2K tokens · pinned 6 days ago
provider-configuration
This skill guides implementing Terraform provider authentication using the Plugin Framework, covering provider schema design, credential provider chains that…
Claude Code, Codex, Cursor +4 · about 3.3K tokens · pinned 6 days ago
Related in Cloud and DevOps
google-agents-cli-deploy
Deploys agents built with agents-cli to Agent Runtime, Cloud Run, or GKE using a tested CLI pipeline.
by GoogleOfficial publisher
Claude Code, Codex, Cursor +4 · about 7.4K tokens · pinned 6 days ago
azure-to-aws
This skill guides agents through migrating Azure workloads to AWS across six phases: discovering Azure resources from Terraform, live CLI, app code, and…
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 4.3K tokens · pinned 6 days ago
gcp-to-aws
Migrates workloads from Google Cloud Platform to AWS, including AI and agentic applications from any provider.
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 14.3K tokens · pinned 6 days ago
heroku-to-aws
This skill guides migration of applications and databases from Heroku to AWS services like Elastic Beanstalk, RDS, ElastiCache, and MSK.
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 7K tokens · pinned 6 days ago
tf-best-practices
This skill provides authoring rules for generating secure AWS Terraform and runs a read-only policy gate to verify generated code against those rules.
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 3K tokens · pinned 6 days ago
provider-ephemeral-resources
Implement Terraform ephemeral resources using the Plugin Framework to handle sensitive values like tokens and credentials that must never be written to state…
by HashiCorpOfficial publisher
Claude Code, Codex, Cursor +4 · about 2.1K tokens · pinned 6 days ago