Implement Terraform Provider actions using the Plugin Framework. Use when developing imperative operations that execute at lifecycle events (before/after create, update, destroy).
65
79%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./plugins/terraform/skills/provider-actions/SKILL.mdTerraform 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:
When adding the first action to a provider that has never had one, several one-time scaffolding steps are required:
ProviderWithActions — add an Actions() method to the provider that returns []func() action.Action.ActionData in Configure — the provider's Configure method must set resp.ActionData = v alongside the existing ResourceData, DataSourceData, and EphemeralResourceData assignments.ActionWithConfigure base type — if the provider uses embedded base types (e.g. ResourceWithConfigure), create an equivalent ActionWithConfigure type implementing action.ConfigureRequest / action.ConfigureResponse.namespace) via helper functions, action-schema variants are needed since action/schema types differ from resource/schema types.Most providers keep actions alongside resources in the provider package:
internal/provider/
├── <action_name>_action.go # Action implementation
└── <action_name>_action_test.go # Action tests(Large multi-service providers use internal/service/<service>/ packages
instead — follow the target repository's layout.)
Documentation lives with the other generated docs:
docs/actions/
└── <action_name>.md # User-facing documentation(Some older, large providers hand-write
website/docs/actions/<name>.html.markdown instead — match the repo.)
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/types.Int64 and schemas use
types.StringType from
github.com/hashicorp/terraform-plugin-framework/types — don't mix in
types from other packagesfwtypes); inside such a repo,
follow its convention consistently instead of the plain typesList/Map Element Types
// WRONG - missing ElementType
"items": schema.ListAttribute{
Optional: true,
}
// CORRECT
"items": schema.ListAttribute{
Optional: true,
ElementType: types.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 (multi-region providers, e.g. AWS)
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)...)
if resp.Diagnostics.HasError() {
return
}
// a.client was stored by Configure (from req.ProviderData), the same
// pattern resources use.
resp.SendProgress(action.InvokeProgressEvent{Message: "Starting operation..."})
// Implement action logic with error handling
// Use context for timeout management
// Poll for completion if async operation
resp.SendProgress(action.InvokeProgressEvent{Message: "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.client), shared with
resources and data sourcesFor operations that require waiting for completion, poll on a ticker under
a context deadline, reporting progress as you go. (Alternatively use
retry.StateChangeConf from
github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry, the same waiter
primitive resources use.)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// Poll fast, report slow: progress events cross the plugin protocol, so
// throttle them instead of emitting one per poll.
start := time.Now()
var lastProgress time.Time
for {
res, err := findResource(ctx, a.client, id)
if err != nil {
resp.Diagnostics.AddError("Error polling operation", fmt.Sprintf("checking status of %s: %s", id, err))
return
}
switch res.Status {
case "AVAILABLE", "COMPLETED":
resp.SendProgress(action.InvokeProgressEvent{Message: "Operation completed"})
return
case "CREATING", "PENDING":
if time.Since(lastProgress) >= 30*time.Second {
lastProgress = time.Now()
resp.SendProgress(action.InvokeProgressEvent{
Message: fmt.Sprintf("Status: %s, Elapsed: %v", res.Status, time.Since(start).Round(time.Second)),
})
}
default:
resp.Diagnostics.AddError("Operation Failed", fmt.Sprintf("%s entered unexpected status %q", id, res.Status))
return
}
select {
case <-ctx.Done():
resp.Diagnostics.AddError("Operation Timed Out", fmt.Sprintf("%s did not complete within %v", id, timeout))
return
case <-ticker.C:
}
}Actions are invoked via action_trigger lifecycle blocks in Terraform configurations. A standalone action block without a corresponding trigger is declared but never executed.
Action parameters must be wrapped in a config {} block. Trigger references use the action. prefix, and actions is a list. Events are bare identifiers, not quoted strings.
action "provider_service_action" "name" {
config {
parameter = value
}
}
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.name]
}
}
}Supported events (as of Terraform 1.14):
before_create - Before resource creationafter_create - After resource creationbefore_update - Before resource updateafter_update - After resource updateNot supported (as of Terraform 1.14; check current release notes):
before_destroy - Not available (will cause validation error)after_destroy - Not available (will cause validation error)func TestAccExampleAction_basic(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_14_0),
},
Steps: []resource.TestStep{
{
Config: testAccActionConfig_basic(),
ConfigStateChecks: []statecheck.StateCheck{
// assert the observable effect of the action on the
// triggering resource
},
},
},
})
}Actions invoked in tests can leave real resources behind; register sweepers
(list → filter test-prefixed names → delete) so leaked resources are
cleanable. Sweepers are not action-specific — use the
provider-test-patterns skill (if available) for the sweep function
pattern, registration, TestMain, and dependency ordering.
terraform_data as a No-Op Triggerterraform_data can serve as a no-op trigger resource for action tests that don't need real infrastructure. This is valuable for error-case and validation tests:
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.test]
}
}
}
action "provider_service_action" "test" {
config {
param = "invalid-value"
}
}PostApplyFunc to Verify Side EffectsActions don't produce state that can be checked with resource.TestCheckResourceAttr. Use PostApplyFunc on resource.TestStep to query the API after apply and confirm the action produced the expected side effect:
Steps: []resource.TestStep{
{
Config: testConfig,
PostApplyFunc: func() {
// query the API to verify the action's side effect occurred
},
},
},Service-Specific Prerequisites
Error Pattern Matching
regexp.MustCompile(\(?s)Error Title.*key phrase`)`Test Patterns Not Applicable to Actions
Compile-check first, then run the focused acceptance test:
go test -c -o /dev/null ./internal/provider
TF_ACC=1 go test ./internal/provider -run TestAccExampleAction_ -timeout 60mUse the run-acceptance-tests skill (if available) for environment variable
setup, debugging failing tests, and sweeper runs.
Generate action documentation with tfplugindocs where the provider uses
it (use the provider-docs skill, if available, for that workflow). Each
action documentation page must include:
Front Matter (hand-written legacy layouts only)
---
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 (optional tooling)
terrafmt, run terrafmt fmt before submission and
verify with terrafmt diffSome providers (e.g. terraform-provider-aws) track release notes with
go-changelog: one file per PR
in a .changelog/ directory. Check the target repo's CONTRIBUTING guide;
skip this if the repo doesn't use it.
.changelog/<pr_number>.txtContent format:
action/provider_service_action: Brief description of the actionBefore submitting your action implementation:
go build -o /dev/null .go test -c -o /dev/null ./internal/providergofmt (or the repo's make fmt)terraform-provider-tfe (action_query_run.go, action_query_run_test.go), terraform-provider-vault (action_rotate_root.go)3268468
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.