---
title: hooks
url: /docs/iac/concepts/resources/options/hooks/
---
The `hooks` resource option provides a set of resource hooks linked to a resource. Hooks are used to execute custom logic at specific points in the resource lifecycle, such as before or after creation, update, or deletion.

> **Applies to custom and component resources.** The `hooks` resource option applies to both [custom resources](/docs/iac/concepts/resources/) and [component resources](/docs/iac/concepts/components/). It is defined on the base resource-options type in every Pulumi SDK. Hooks attached to a component fire on the component's own create/update/delete lifecycle events. They are not automatically inherited by the component's child resources — attach hooks to each child individually if you want them to fire there as well.

> **Note:** Resource hooks are supported in TypeScript/JavaScript, Python, Go, and C#/.NET. Java and YAML do not support resource hooks.

Hooks can be attached to both custom resources and component resources. A hook attached to a component fires on the component's own create, update, and delete lifecycle events. Hooks are not automatically inherited by a component's child resources — attach hooks to each child individually if you want them to fire there as well.

Each hook is a callback that gets invoked by the Pulumi engine. Hooks that execute before an action are called **before hooks** and have names beginning with `before` or `Before` depending on the language. Hooks that execute after an action are called **after hooks** and have names beginning with `after` or `After` depending on the language. Pulumi currently supports the following hook types:

* *Create hooks* are called before or after a resource is created. This may occur during the initial creation of a resource or when a resource requires replacement due to e.g. a change in an immutable property.

* *Update hooks* are called before or after a resource is updated in-place.

* *Delete hooks* are called before or after a resource is deleted. This may occur during the deletion of a resource due to a `destroy` or that resource's removal from the program, or as part of a resource replacement due to e.g. a change in an immutable property.

When a hook is executed as part of a resource operation, it receives the resource's [URN](/docs/iac/concepts/resources/names/#urns) and ID, as well as any relevant input and output properties. Hooks may return errors. If a before hook returns an error, the action it precedes will *not* be executed and the Pulumi operation will fail with that error. If an after hook returns an error, the underlying resource operation has already completed and is recorded in the state, but the deployment is then failed. To opt out of this behavior and instead log a warning and continue, set the [`ignoreErrors`](#ignoring-hook-errors) option on the hook. The table below illustrates the combinations of inputs, outputs, and error behaviors for each hook type:

| Hook type     | Old inputs | New inputs | Old outputs | New outputs | Error behavior                                 |
|---------------|------------|------------|-------------|-------------|------------------------------------------------|
| Before create |            | ✓          |             |             | Prevent creation, fail deployment              |
| After create  |            | ✓          |             | ✓           | Record resource in state, then fail deployment |
| Before update | ✓          | ✓          | ✓           |             | Prevent update, fail deployment                |
| After update  | ✓          | ✓          | ✓           | ✓           | Record update in state, then fail deployment   |
| Before delete | ✓          |            | ✓           |             | Prevent deletion, fail deployment              |
| After delete  | ✓          |            | ✓           |             | Record deletion in state, then fail deployment |

> **Note:** Before Pulumi v3.238.0, after hooks that returned an error logged a warning and the deployment continued. Starting with v3.238.0, an after-hook failure fails the deployment by default. Use the `ignoreErrors` hook option to restore the previous behavior, or pass `--continue-on-error` to `pulumi up`/`pulumi destroy` to keep processing other resources after a hook failure.

## Health checking example

<!-- chooser: language -->

<!-- option: typescript -->
```typescript
import * as aws from "@pulumi/aws"
import * as pulumi from "@pulumi/pulumi"

export = async () => {
  // Define the instance's user data script to set up a simple HTTP server that
  // serves some static content. We'll try to fetch the health.json in a
  // lifecycle hook later on to check whether or not the server is up and running.
  const userData = `#!/bin/bash
echo "Hello, World!" > index.html
echo '{"ok": true}' > health.json
nohup python3 -m http.server 80 &`

  // Define a resource hook that will run after create and update and not return
  // until the health check passes.
  const afterHook = new pulumi.ResourceHook("after", async args => {
    // Since this is an after hook, we'll have access to the new outputs of the
    // resource.
    const outputs = args.newOutputs as aws.ec2.InstanceState

    // Attempt to fetch health.json from the instance's public endpoint, backing
    // off linearly if it is not yet available.
    const maxRetries = 30
    for (let i = 0; i < maxRetries; i++) {
      try {
        const response = await fetch(`http://${outputs.publicDns}/health.json`)
        if (response.ok) {
          const data = await response.json()
          console.log(`Health check passed: ${JSON.stringify(data)}`)
          return
        }
      } catch (error) {
        console.log(`Health check attempt ${i + 1} failed`)
      }

      await new Promise(resolve => setTimeout(resolve, (i + 1) * 1000))
    }
  })

  // Set up the resources needed to run the web server, as outlined in the
  // how-to guide linked above.

  const instanceType = "t2.micro"
  const ami = aws.ec2.getAmiOutput({
    filters: [{
      name: "name",
      values: ["amzn2-ami-hvm-*"],
    }],
    // Amazon owns this AMI so we'll use their owner ID.
    owners: ["137112412989"],
    mostRecent: true,
  })

  const group = new aws.ec2.SecurityGroup("webserver-secgrp", {
    ingress: [{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] },
      { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
    ],
  })

  const server = new aws.ec2.Instance("webserver-www", {
    instanceType,
    vpcSecurityGroupIds: [group.id],
    ami: ami.id,
    userData,
  }, {
    hooks: {
      afterCreate: [afterHook],
      afterUpdate: [afterHook],
    },
  })

  return {
    publicDns: server.publicDns,
    publicIp: server.publicIp,
  }
}

```

<!-- /option -->

<!-- option: python -->
```python
import json
import time

import pulumi
import requests
from pulumi_aws import ec2

# Define the instance's user data script to set up a simple HTTP server that
# serves some static content. We'll try to fetch the health.json in a
# lifecycle hook later on to check whether or not the server is up and running.
user_data = """#!/bin/bash
echo "Hello, World!" > index.html
echo '{"ok": true}' > health.json
nohup python3 -m http.server 80 &"""

# Define a resource hook that will run after create and update and not return
# until the health check passes.
def health_check(args: pulumi.ResourceHookArgs):
    # Since this is an after hook, we'll have access to the new outputs of the
    # resource.
    outputs = args.new_outputs

    # Attempt to fetch health.json from the instance's public endpoint, backing
    # off linearly if it is not yet available.
    max_retries = 30
    for i in range(max_retries):
        try:
            response = requests.get(
                f"http://{outputs['publicDns']}/health.json", timeout=10
            )
            if response.status_code == 200:
                data = response.json()
                print(f"Health check passed: {json.dumps(data)}")
                return
        except Exception as error:
            print(f"Health check attempt {i + 1} failed: {error}")

        # Linear backoff - wait (i + 1) seconds before next attempt
        time.sleep(i + 1)

instance_type = "t2.micro"

ami = ec2.get_ami_output(
    filters=[{
            "name": "name",
            "values": ["amzn2-ami-hvm-*"],
        }
    ],
    # Amazon owns this AMI so we'll use their owner ID.
    owners=["137112412989"],
    most_recent=True,
)

group = ec2.SecurityGroup(
    "webserver-secgrp",
    ingress=[{
            "protocol": "tcp",
            "from_port": 22,
            "to_port": 22,
            "cidr_blocks": ["0.0.0.0/0"],
        },
        {
            "protocol": "tcp",
            "from_port": 80,
            "to_port": 80,
            "cidr_blocks": ["0.0.0.0/0"],
        },
    ],
)

server = ec2.Instance(
    "webserver-www",
    instance_type=instance_type,
    vpc_security_group_ids=[group.id],
    ami=ami.id,
    user_data=user_data,
    opts=pulumi.ResourceOptions(
        hooks=pulumi.ResourceHookBinding(
            after_create=[health_check],
            after_update=[health_check],
        ),
    ),
)

```

<!-- /option -->

<!-- option: go -->
```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

// Define a resource hook that will run after create and update and not return
// until the health check passes.
func healthCheck(args *pulumi.ResourceHookArgs) error {
	// Since this is an after hook, we'll have access to the new outputs of the
	// resource.
	publicDns := args.NewOutputs["publicDns"].StringValue()

	// Attempt to fetch health.json from the instance's public endpoint, backing
	// off linearly if it is not yet available.
	maxRetries := 30
	for i := 0; i < maxRetries; i++ {
		url := fmt.Sprintf("http://%s/health.json", publicDns)

		client := &http.Client{
			Timeout: 10 * time.Second,
		}

		resp, err := client.Get(url)
		if err == nil && resp.StatusCode == 200 {
			var data map[string]interface{}
			if err := json.NewDecoder(resp.Body).Decode(&data); err == nil {
				resp.Body.Close()
				dataJSON, _ := json.Marshal(data)
				fmt.Printf("Health check passed: %s\n", string(dataJSON))
				return nil
			}
			resp.Body.Close()
		}

		if resp != nil {
			resp.Body.Close()
		}

		fmt.Printf("Health check attempt %d failed: %v\n", i+1, err)

		// Linear backoff - wait (i + 1) seconds before next attempt
		time.Sleep(time.Duration(i+1) * time.Second)
	}

	return fmt.Errorf("health check failed after %d attempts", maxRetries)
}

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// Define the instance's user data script to set up a simple HTTP server
		// that serves some static content. We'll try to fetch the health.json
		// in a lifecycle hook later on to check whether or not the server is up
		// and running.
		userData := `#!/bin/bash
echo "Hello, World!" > index.html
echo '{"ok": true}' > health.json
nohup python3 -m http.server 80 &`

		instanceType := "t2.micro"

		ami, err := ec2.LookupAmi(ctx, &ec2.LookupAmiArgs{
			Filters: []ec2.GetAmiFilter{
				{
					Name:   "name",
					Values: []string{"amzn2-ami-hvm-*"},
				},
			},
			// Amazon owns this AMI so we'll use their owner ID.
			Owners:     []string{"137112412989"},
			MostRecent: pulumi.BoolRef(true),
		})
		if err != nil {
			return err
		}

		group, err := ec2.NewSecurityGroup(ctx, "webserver-secgrp", &ec2.SecurityGroupArgs{
			Ingress: ec2.SecurityGroupIngressArray{
				&ec2.SecurityGroupIngressArgs{
					Protocol:   pulumi.String("tcp"),
					FromPort:   pulumi.Int(22),
					ToPort:     pulumi.Int(22),
					CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				},
				&ec2.SecurityGroupIngressArgs{
					Protocol:   pulumi.String("tcp"),
					FromPort:   pulumi.Int(80),
					ToPort:     pulumi.Int(80),
					CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				},
			},
		})
		if err != nil {
			return err
		}

		hook, err := ctx.RegisterResourceHook("health-check", healthCheck, nil)
		if err != nil {
			return err
		}

		server, err := ec2.NewInstance(ctx, "webserver-www", &ec2.InstanceArgs{
			InstanceType:        pulumi.String(instanceType),
			VpcSecurityGroupIds: pulumi.StringArray{group.ID()},
			Ami:                 pulumi.String(ami.Id),
			UserData:            pulumi.String(userData),
		}, pulumi.ResourceHooks(&pulumi.ResourceHookBinding{
			AfterCreate: []*pulumi.ResourceHook{hook},
			AfterUpdate: []*pulumi.ResourceHook{hook},
		}))
		if err != nil {
			return err
		}

		ctx.Export("publicDns", server.PublicDns)
		ctx.Export("publicIp", server.PublicIp)

		return nil
	})
}

```

<!-- /option -->

<!-- option: csharp -->
```csharp
using System;

using System.Threading.Tasks;
using Pulumi;

using Pulumi.Aws.Ec2;
using Pulumi.Aws.Ec2.Inputs;
using Pulumi.Command.Local;
using System.Diagnostics;
using System.Collections.Generic;

class Program
{
    static Task<int> Main() => Deployment.RunAsync<MyStack>();
}

class MyStack : Stack
{
    public MyStack()
    {
        // Define the instance's user data script to set up a simple HTTP server that
        // serves some static content. We'll try to fetch the health.json in a
        // lifecycle hook later on to check whether or not the server is up and running.
        var userData = @"#!/bin/bash
echo ""Hello, World!"" > index.html
echo '{""ok"": true}' > health.json
nohup python3 -m http.server 80 &";

        // Define a resource hook that will run after create and update and not return
        // until the health check passes.
        var afterHook = new ResourceHook("after", async (args, cancellationToken) =>
        {
            // Since this is an after hook, we'll have access to the new outputs of the
            // resource.
            var outputs = args.NewOutputs

            // Attempt to fetch health.json from the instance's public endpoint, backing
            // off linearly if it is not yet available.
            const int maxRetries = 30;
            for (var i = 0; i < maxRetries; i++)
            {
                try
                {
                    using var client = new HttpClient();
                    var response = await client.GetAsync($"http://{outputs?["publicDns"]}/health.json");
                    if (response.IsSuccessStatusCode)
                    {
                        var data = await response.Content.ReadAsStringAsync();
                        Console.WriteLine($"Health check passed: {data}");
                        return;
                    }
                }
                catch (Exception)
                {
                    Console.WriteLine($"Health check attempt {i + 1} failed");
                }

                await Task.Delay((i + 1) * 1000);
            }
        });

        // Set up the resources needed to run the web server, as outlined in the
        // how-to guide linked above.

        var instanceType = "t2.micro";
        var ami = GetAmi.Invoke(new GetAmiArgs
        {
            Filters =
            {
                new GetAmiFilterArgs
                {
                    Name = "name",
                    Values = { "amzn2-ami-hvm-*" },
                },
            },
            Owners = { "137112412989" }, // Amazon owns this AMI so we'll use their owner ID.
            MostRecent = true,
        });

        var group = new SecurityGroup("webserver-secgrp", new SecurityGroupArgs
        {
            Ingress =
            {
                new SecurityGroupIngressArgs
                {
                    Protocol = "tcp",
                    FromPort = 22,
                    ToPort = 22,
                    CidrBlocks = { "0.0.0.0/0" },
                },
                new SecurityGroupIngressArgs
                {
                    Protocol = "tcp",
                    FromPort = 80,
                    ToPort = 80,
                    CidrBlocks = { "0.0.0.0/0" },
                },
            },
        });

        var server = new Instance("webserver-www", new InstanceArgs
        {
            InstanceType = instanceType,
            VpcSecurityGroupIds = { group.Id },
            Ami = ami.Apply(a => a.Id),
            UserData = userData,
        }, new CustomResourceOptions
        {
            Hooks =
            {
                AfterCreate = { afterHook },
                AfterUpdate = { afterHook },
            }
        });

        // Export the public DNS and IP of the server.
        this.PublicDns = server.PublicDns;
        this.PublicIp = server.PublicIp;
    }

    [Output]
    public Output<string> PublicDns { get; private set; }

    [Output]
    public Output<string> PublicIp { get; private set; }
}

```

<!-- /option -->

<!-- option: java -->
```java
// Pulumi Java does not support resource hooks

```

<!-- /option -->

<!-- option: yaml -->
```yaml
# Pulumi YAML does not support resource hooks

```

<!-- /option -->

<!-- /chooser -->

## Ignoring hook errors

By default, when any hook (before or after) returns an error, the Pulumi deployment fails. For after hooks, the underlying resource operation has already succeeded by the time the hook runs, so its result is recorded in the state before the deployment is failed.

If a hook failure should not be treated as fatal — for example, a best-effort notification or audit step — set the `ignoreErrors` option when constructing the hook. Errors from that hook will then be logged as warnings and the deployment will continue.

<!-- chooser: language -->

<!-- option: typescript -->
```typescript
import * as pulumi from "@pulumi/pulumi";

const notifyHook = new pulumi.ResourceHook("notify", async args => {
    // Best-effort notification. Failures here should not fail the deployment.
    await sendNotification(args);
}, {
    ignoreErrors: true,
});

```

<!-- /option -->

<!-- option: python -->
```python
import pulumi

def notify(args: pulumi.ResourceHookArgs):
    # Best-effort notification. Failures here should not fail the deployment.
    send_notification(args)

notify_hook = pulumi.ResourceHook(
    "notify",
    notify,
    opts=pulumi.ResourceHookOptions(ignore_errors=True),
)

```

<!-- /option -->

<!-- option: go -->
```go
package main

import (
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
    pulumi.Run(func(ctx *pulumi.Context) error {
        _, err := ctx.RegisterResourceHook(
            "notify",
            func(args *pulumi.ResourceHookArgs) error {
                // Best-effort notification. Failures here should not fail the deployment.
                return sendNotification(args)
            },
            &pulumi.ResourceHookOptions{
                IgnoreErrors: true,
            },
        )
        if err != nil {
            return err
        }
        return nil
    })
}

```

<!-- /option -->

<!-- option: csharp -->
```csharp
using Pulumi;

var notifyHook = new ResourceHook(
    "notify",
    async (args, cancellationToken) =>
    {
        // Best-effort notification. Failures here should not fail the deployment.
        await SendNotificationAsync(args, cancellationToken);
    },
    new ResourceHookOptions
    {
        IgnoreErrors = true,
    });

```

<!-- /option -->

<!-- option: java -->
```java
// Pulumi Java does not support resource hooks

```

<!-- /option -->

<!-- option: yaml -->
```yaml
# Pulumi YAML does not support resource hooks

```

<!-- /option -->

<!-- /chooser -->

To keep applying other resources after a hook failure on a single update, you can also run `pulumi up` (or `pulumi destroy`) with the `--continue-on-error` flag. This applies to the whole deployment, not only to hooks.

## Deletions and delete hooks

In order for delete hooks to run successfully, Pulumi must have access to any necessary hooks at the time of the deletion. You should take the following actions to ensure that delete hooks run as expected:

* When removing resources from your program, first remove *only* the resources you wish to delete, *leaving any delete hooks in place*. Upon running e.g. `pulumi up`, Pulumi will delete the resources and run any relevant delete hooks. Once this operation is complete, you can then remove the delete hooks from your program.

* When running `pulumi destroy`, you must pass the `--run-program` flag to instruct Pulumi to run your program and register any hooks that are to be executed. If Pulumi detects that you are trying to `destroy` a stack that contains hooks _without_ the `--run-program` flag, it will fail with an error. See [Running your program on refresh and destroy](/docs/iac/operations/stack-management/run-program/) for other situations where the flag is useful.

## Error hooks

Just as the other resource hooks can be executed before and after certain operations, you can also add hooks to run when operations fail. For example, to retry a failing resource registration, or to change the error-handling behavior based on the type of error encountered. The inputs and outputs received will depend on the operation that fails:

| Failed operation | Old inputs | New inputs | Old outputs |
|------------------|------------|------------|-------------|
| `create`         |            | ✓          |             |
| `update`         | ✓          | ✓          | ✓           |
| `delete`         | ✓          |            | ✓           |

As well as the standard hook information and the name of the failing operation, error hooks also receive a list of errors encountered during previous runs (starting with the most recent). In other words, if a resource has failed three times, the hook receives three errors. The hook must then reply with a flag that determines whether to retry the operation, or whether to let the failure cascade and exit the program.

<!-- chooser: language -->

<!-- option: typescript -->
```typescript
import * as pulumi from "@pulumi/pulumi";

const notStartedRetryHook = new pulumi.ErrorHook(
    "retry-when-not-started",
    async (args) => {
        const latestError = args.errors[0] ?? "";

        if (!latestError.includes("resource has not yet started")) {
          return false; // do not retry, this is another type of error
        }

        await new Promise((resolve) => setTimeout(resolve, 5000));
        return true; // retry
    },
);

const res = new MyResource("res", {}, {
    hooks: {
        onError: [notStartedRetryHook],
    },
});

```

<!-- /option -->

<!-- option: python -->
```python
import time

import pulumi

def retry_when_not_started(args: pulumi.ErrorHookArgs) -> bool:
    latest_error = args.errors[0] if args.errors else ""

    if "resource has not yet started" not in latest_error:
        return False # do not retry, this is another type of error

    time.sleep(5)
    return True # retry

not_started_retry_hook = pulumi.ErrorHook(
    "retry-when-not-started",
    retry_when_not_started,
)

res = MyResource(
    "res",
    opts=pulumi.ResourceOptions(
        hooks=pulumi.ResourceHookBinding(
            on_error=[not_started_retry_hook],
        ),
    ),
)

```

<!-- /option -->

<!-- option: go -->
```go
package main

import (
    "strings"
    "time"

    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
    pulumi.Run(func(ctx *pulumi.Context) error {
        hook, err := ctx.RegisterErrorHook(
            "retry-when-not-started",
            func(args *pulumi.ErrorHookArgs) (bool, error) {
                latest := ""
                if len(args.Errors) > 0 {
                    latest = args.Errors[0]
                }

                if !strings.Contains(latest, "resource has not yet started") {
                    return false, nil // do not retry, this is another type of error
                }

                time.Sleep(5 * time.Second)
                return true, nil // retry
            },
        )
        if err != nil {
            return err
        }

        _, err = NewMyResource(ctx, "res", &MyResourceArgs{}, pulumi.ResourceHooks(&pulumi.ResourceHookBinding{
            OnError: []*pulumi.ErrorHook{hook},
        }))
        if err != nil {
            return err
        }

        return nil
    })
}

```

<!-- /option -->

<!-- option: csharp -->
```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Pulumi;

class ErrorHookStack : Stack
{
    public ErrorHookStack()
    {
        var retryHook = new ErrorHook(
            "retry-when-not-started",
            async (args, cancellationToken) =>
            {
                var latestError = args.Errors.Count > 0 ? args.Errors[0] : "";

                if (!latestError.Contains("resource has not yet started"))
                {
                    return false; // do not retry, this is another type of error
                }

                await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
                return true; // retry
            });

        var res = new MyResource("res", new MyResourceArgs(), new CustomResourceOptions
        {
            Hooks = new ResourceHookBinding
            {
                OnError = { retryHook },
            },
        });
    }
}

```

<!-- /option -->

<!-- option: java -->
```java
// Pulumi Java does not support resource hooks

```

<!-- /option -->

<!-- option: yaml -->
```yaml
# Pulumi YAML does not support resource hooks

```

<!-- /option -->

<!-- /chooser -->

> **Note:** An operation can only be retried a maximum of 100 times. After this, the engine will report the failure as a program failure, and the deployment will fail as normal.


