Implements Terraform Provider actions using Plugin Framework for imperative operations at lifecycle events like before/after create, update, destroy.
From terraform-provider-developmentnpx claudepluginhub hashicorp/agent-skills --plugin terraform-provider-developmentThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides implementation of event-driven hooks in Claude Code plugins using prompt-based validation and bash commands for PreToolUse, Stop, and session events.
Terraform Actions enable imperative operations during the Terraform lifecycle. Actions are experimental features that allow performing provider operations at specific lifecycle events (before/after create, update, destroy).
References:
Actions follow the standard service package structure:
internal/service/<service>/
├── <action_name>_action.go # Action implementation
├── <action_name>_action_test.go # Action tests
└── service_package_gen.go # Auto-generated service registration
Documentation structure:
website/docs/actions/
└── <service>_<action_name>.html.markdown # User-facing documentation
Changelog entry:
.changelog/
└── <pr_number_or_description>.txt # Release note entry
Actions use the Terraform Plugin Framework with a standard schema pattern:
func (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
// Required configuration parameters
"resource_id": schema.StringAttribute{
Required: true,
Description: "ID of the resource to operate on",
},
// Optional parameters with defaults
"timeout": schema.Int64Attribute{
Optional: true,
Description: "Operation timeout in seconds",
Default: int64default.StaticInt64(1800),
Computed: true,
},
},
}
}
Pay special attention to the schema definition - common issues after a first draft:
Type Mismatches
types.String instead of fwtypes.String in model structstypes.StringType instead of fwtypes.StringType in schemaList/Map Element Types
// WRONG - missing ElementType
"items": schema.ListAttribute{
Optional: true,
}
// CORRECT
"items": schema.ListAttribute{
Optional: true,
ElementType: fwtypes.StringType,
}
Computed vs Optional
Optional: true and Computed: trueComputed unless they have defaultsValidator Imports
// Ensure proper imports
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
Region/Provider Attribute
Nested Attributes
Before submitting, verify:
go build to catch type mismatchesThe Invoke method contains the action logic:
func (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
var data actionModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
// Create provider client
conn := a.Meta().Client(ctx)
// Progress updates for long-running operations
resp.Progress.Set(ctx, "Starting operation...")
// Implement action logic with error handling
// Use context for timeout management
// Poll for completion if async operation
resp.Progress.Set(ctx, "Operation completed")
}
resp.SendProgress(action.InvokeProgressEvent{...}) for real-time updatescontext.WithTimeout() for API callsresp.Diagnostics.AddError()Example error handling:
// Handle specific errors
var notFound *types.ResourceNotFoundException
if errors.As(err, ¬Found) {
resp.Diagnostics.AddError(
"Resource Not Found",
fmt.Sprintf("Resource %s was not found", resourceID),
)
return
}
// Generic error handling
resp.Diagnostics.AddError(
"Operation Failed",
fmt.Sprintf("Could not complete operation for %s: %s", resourceID, err),
)
a.Meta().<Service>Client(ctx)For operations that require waiting for completion:
result, err := wait.WaitForStatus(ctx,
func(ctx context.Context) (wait.FetchResult[*ResourceType], error) {
// Fetch current status
resource, err := findResource(ctx, conn, id)
if err != nil {
return wait.FetchResult[*ResourceType]{}, err
}
return wait.FetchResult[*ResourceType]{
Status: wait.Status(resource.Status),
Value: resource,
}, nil
},
wait.Options[*ResourceType]{
Timeout: timeout,
Interval: wait.FixedInterval(5 * time.Second),
SuccessStates: []wait.Status{"AVAILABLE", "COMPLETED"},
TransitionalStates: []wait.Status{"CREATING", "PENDING"},
ProgressInterval: 30 * time.Second,
ProgressSink: func(fr wait.FetchResult[any], meta wait.ProgressMeta) {
resp.SendProgress(action.InvokeProgressEvent{
Message: fmt.Sprintf("Status: %s, Elapsed: %v", fr.Status, meta.Elapsed.Round(time.Second)),
})
},
},
)
Actions are invoked via action_trigger lifecycle blocks in Terraform configurations:
action "provider_service_action" "name" {
config {
parameter = value
}
}
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.name]
}
}
}
Terraform 1.14.0 Supported Events:
before_create - Before resource creationafter_create - After resource creationbefore_update - Before resource updateafter_update - After resource updateNot Supported in Terraform 1.14.0:
before_destroy - Not available (will cause validation error)after_destroy - Not available (will cause validation error)func TestAccServiceAction_basic(t *testing.T) {
ctx := acctest.Context(t)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_14_0),
},
Steps: []resource.TestStep{
{
Config: testAccActionConfig_basic(),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourceExists(ctx, "provider_resource.test"),
),
},
},
})
}
Add sweep functions to clean up test resources:
func sweepResources(region string) error {
ctx := context.Background()
client := /* get client for region */
input := &service.ListInput{
// Filter for test resources
}
var sweeperErrs *multierror.Error
pages := service.NewListPaginator(client, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)
if err != nil {
sweeperErrs = multierror.Append(sweeperErrs, err)
continue
}
for _, item := range page.Items {
id := item.Id
// Skip non-test resources
if !strings.HasPrefix(id, "tf-acc-test") {
continue
}
_, err := client.Delete(ctx, &service.DeleteInput{
Id: id,
})
if err != nil {
sweeperErrs = multierror.Append(sweeperErrs, err)
}
}
}
return sweeperErrs.ErrorOrNil()
}
Service-Specific Prerequisites
Error Pattern Matching
regexache.MustCompile(\(?s)Error Title.*key phrase`)`Test Patterns Not Applicable to Actions
Compile test to check for errors:
go test -c -o /dev/null ./internal/service/<service>
Run specific action tests:
TF_ACC=1 go test ./internal/service/<service> -run TestAccServiceAction_ -v
Run sweep to clean up test resources:
TF_ACC=1 go test ./internal/service/<service> -sweep=<region> -v
Each action documentation file must include:
Front Matter
---
subcategory: "Service Name"
layout: "provider"
page_title: "Provider: provider_service_action"
description: |-
Brief description of what the action does.
---
Header with Warnings
Example Usage
terraform_dataArgument Reference
Documentation Linting
terrafmt fmt before submissionterrafmt diffCreate a changelog entry in .changelog/ directory:
.changelog/<pr_number_or_description>.txt
Content format:
action/provider_service_action: Brief description of the action
Before submitting your action implementation:
go build -o /dev/null .go test -c -o /dev/null ./internal/service/<service>make fmtterrafmt fmt website/docs/actions/<action>.html.markdown