# Testing HTTP Triggers in Simulation
Source: https://docs.chain.link/cre/guides/workflow/using-triggers/http-trigger/testing-in-simulation
Last Updated: 2026-10-30

> For the complete documentation index, see [llms.txt](/llms.txt).

During development, you can test your HTTP trigger workflows locally using the `cre workflow simulate` command. The simulator allows you to provide test payloads without setting up authorization keys or JWT authentication.

This guide focuses specifically on HTTP trigger simulation with detailed examples and scenarios. For a general overview of workflow simulation covering all trigger types, see [Simulating Workflows](/cre/guides/operations/simulating-workflows).

## Prerequisites

Before running a simulation:

- **CRE CLI installed**: You need the CRE CLI to run simulations. See [CLI Installation](/cre/getting-started/cli-installation) if you haven't installed it yet.
- **CRE account & authentication**: You must have a CRE account and be logged in with the CLI. See [Create your account](/chainlink-platform/creating-an-account?parent=cre) and [Log in with the CLI](/cre/guides/cli/cli-login) for instructions.
- **HTTP trigger configured**: Your workflow must have an HTTP trigger handler registered. See [Configuration & Handler](/cre/guides/workflow/using-triggers/http-trigger/configuration) for setup instructions.

> **NOTE: No authorization required for simulation**
>
> During simulation, you can use an empty authorization configuration (e.g., `&http.Config{}` in Go or `trigger({})` in
> TypeScript). This simplifies local testing—just remember to add `authorizedKeys` before deploying.

## Basic simulation

To simulate a workflow with an HTTP trigger, run the `simulate` command from your project root:

```bash
cre workflow simulate <workflow-folder> --target <target-name>
```

**Example:**

```bash
cre workflow simulate my-http-workflow --target staging-settings
```

The simulator will detect your HTTP trigger and prompt you to select it from available triggers.

## Listen mode

Use `--listen` to keep the simulator alive between runs. Instead of simulating once and exiting, the CLI starts a local HTTP server on port `2000` and re-runs the simulator every time it receives a POST request — no need to restart between test payloads.

```bash
cre workflow simulate my-http-workflow --listen --target staging-settings
```

Once running, send payloads to `/trigger` with any HTTP client. Wrap your payload in an `input` field:

```bash
curl -X POST http://localhost:2000/trigger \
  -H "Content-Type: application/json" \
  -d '{"input":{"userId":"123","action":"purchase","amount":100}}'
```

Your workflow receives the value of `input` as the trigger payload. Each POST triggers a simulation run. Press `Ctrl+C` to stop.

> **NOTE: Restart to pick up code changes**
>
> The simulator compiles your workflow once, when it starts. If you change your workflow code, restart the simulator. To
> avoid recompiling on every start, see [Simulating with a pre-built
> binary](/cre/guides/operations/simulating-workflows#simulating-with-a-pre-built-binary).

> **CAUTION: Default limits skip requests sent less than 30 seconds apart**
>
> By default, the simulator enforces the production [HTTP trigger quota](/cre/service-quotas#http-trigger): one run per
> 30 seconds. In listen mode, the simulator skips requests that arrive sooner and still returns `200 OK`. The only sign
> is a `Trigger rate limited, skipping execution` message in the terminal. To send requests more often, add `--limits
>   none`. See [Testing Production Limits](/cre/guides/operations/understanding-limits).

For the full list of listen mode behavior, including queuing and error handling, see [Listen mode](/cre/guides/operations/simulating-workflows#listen-mode).

## Providing input data

You have three ways to provide JSON input to your HTTP trigger during simulation:

### 1. Interactive mode (default)

When you run the simulation without input flags, the CLI prompts you to enter JSON data:

```bash
$ cre workflow simulate my-http-workflow --target staging-settings

# Select the HTTP trigger when prompted
? Select a trigger to simulate: HTTP Trigger

# Enter your JSON input:
? Enter JSON input for the HTTP trigger:
{"userId": "123", "action": "purchase", "amount": 50}
```

The simulator converts your JSON into a payload and passes it to your callback function.

### 2. Inline JSON string

For non-interactive execution (useful for CI/CD or scripting), pass JSON directly using the `--http-payload` flag along with `--non-interactive` and `--trigger-index`:

```bash
cre workflow simulate my-http-workflow --non-interactive --trigger-index 0 --http-payload '{"userId":"123","action":"purchase","amount":50}' --target staging-settings
```

> **NOTE: Non-interactive requirements**
>
> The `--http-payload` flag requires `--non-interactive` mode. You must also specify `--trigger-index` to select which
> handler to run. The index is 0-based: if the handler with your HTTP trigger is the first handler defined in your
> `InitWorkflow` function, use `--trigger-index 0`; if it's the second, use `--trigger-index 1`, and so on.

> **NOTE: Escaping quotes**
>
> When passing JSON inline, use single quotes around the entire JSON string and double quotes for JSON keys and string
> values. On Windows, you may need to escape double quotes differently: `"{\"userId\":\"123\"}"`

### 3. Input from file

For complex payloads or reusable test data, store your JSON in a file and reference it in non-interactive mode.

**Option 1: File in workflow folder**

Create the payload file in your workflow directory:

**`my-http-workflow/test-payload.json`:**

```json
{
  "userId": "123",
  "action": "purchase",
  "amount": 50,
  "metadata": {
    "timestamp": "2025-11-10T10:00:00Z",
    "source": "mobile-app"
  }
}
```

**Simulate:**

```bash
cre workflow simulate my-http-workflow --non-interactive --trigger-index 0 --http-payload test-payload.json --target staging-settings
```

**Option 2: File at project root**

Create the payload file at your project root. Simulate (using `../` to reference the parent directory):

```bash
cre workflow simulate my-http-workflow --non-interactive --trigger-index 0 --http-payload ../test-payload.json --target staging-settings
```

> **NOTE: File paths are relative to the workflow folder**
>
> The CLI resolves file paths relative to the **workflow folder** you're simulating. Use just the filename for files in
> the workflow folder, or `../filename.json` for files at the project root.

## Example workflow simulation

Let's simulate a complete workflow that processes HTTP requests.

**Setup your config file:**

For this example, create a `config.staging.json` file with:

```json
{
  "minimumAmount": 10
}
```

This configuration sets the minimum purchase amount to 10, which we'll test with different scenarios.

**Workflow code:**

### HTTP Trigger Simulation Example

```go
//go:build wasip1

package main

import (
	"encoding/json"
	"fmt"
	"log/slog"

	"github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http"
	"github.com/smartcontractkit/cre-sdk-go/cre"
	"github.com/smartcontractkit/cre-sdk-go/cre/wasm"
)

type Config struct {
	MinimumAmount int `json:"minimumAmount"`
}

type RequestData struct {
	UserID string `json:"userId"`
	Action string `json:"action"`
	Amount *int   `json:"amount"` // Pointer to detect missing field
}

func onHttpTrigger(config *Config, runtime cre.Runtime, payload *http.Payload) (string, error) {
	var data RequestData
	if err := json.Unmarshal(payload.Input, &data); err != nil {
		return "", fmt.Errorf("failed to parse input: %w", err)
	}

	// Validate required fields
	if data.UserID == "" || data.Action == "" || data.Amount == nil {
		runtime.Logger().Info("Missing required fields")
		return "Error: Missing required fields (userId, action, amount)", nil
	}

	runtime.Logger().Info("Processing request", "action", data.Action, "userId", data.UserID)

	if *data.Amount < config.MinimumAmount {
		runtime.Logger().Info("Amount below minimum", "amount", *data.Amount, "minimum", config.MinimumAmount)
		return "Amount too low", nil
	}

	runtime.Logger().Info("Processing amount", "amount", *data.Amount)
	return fmt.Sprintf("Successfully processed %s", data.Action), nil
}

func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
	httpTrigger := http.Trigger(&http.Config{}) // Empty config OK for simulation

	return cre.Workflow[*Config]{
		cre.Handler(httpTrigger, onHttpTrigger),
	}, nil
}

func main() {
	wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
}
```

### HTTP Trigger Simulation Example

```ts
import { HTTPCapability, handler, type Runtime, type HTTPPayload, Runner, decodeJson } from "@chainlink/cre-sdk"

type Config = {
  minimumAmount: number
}

type RequestData = {
  userId: string
  action: string
  amount: number
}

const onHttpTrigger = (runtime: Runtime<Config>, payload: HTTPPayload): string => {
  const requestData = decodeJson(payload.input) as RequestData

  // Validate required fields
  if (!requestData.userId || !requestData.action || requestData.amount === undefined) {
    runtime.log("Missing required fields")
    return "Error: Missing required fields (userId, action, amount)"
  }

  runtime.log(`Processing ${requestData.action} for user ${requestData.userId}`)

  if (requestData.amount < runtime.config.minimumAmount) {
    runtime.log(`Amount ${requestData.amount} below minimum ${runtime.config.minimumAmount}`)
    return "Amount too low"
  }

  runtime.log(`Processing amount: ${requestData.amount}`)
  return `Successfully processed ${requestData.action}`
}

const initWorkflow = (config: Config) => {
  const http = new HTTPCapability()

  return [
    handler(http.trigger({}), onHttpTrigger), // Empty config OK for simulation
  ]
}

export async function main() {
  const runner = await Runner.newRunner<Config>()
  await runner.run(initWorkflow)
}
```

**Run the simulation:**

```bash
cre workflow simulate my-http-workflow --non-interactive --trigger-index 0 --http-payload '{"userId":"user_123","action":"purchase","amount":100}' --target staging-settings
```

**Expected output:**

```
Workflow compiled
2025-11-10T11:28:25Z [SIMULATION] Simulator Initialized

2025-11-10T11:28:25Z [SIMULATION] Running trigger trigger=http-trigger@1.0.0-alpha
2025-11-10T11:28:25Z [USER LOG] Processing purchase for user user_123
2025-11-10T11:28:25Z [USER LOG] Processing amount: 100

Workflow Simulation Result:
 "Successfully processed purchase"

2025-11-10T11:28:25Z [SIMULATION] Execution finished signal received
2025-11-10T11:28:25Z [SIMULATION] Skipping WorkflowEngineV2
```

## Testing different scenarios

Use simulation to test various input scenarios:

### Valid request

```bash
cre workflow simulate my-http-workflow --non-interactive --trigger-index 0 --http-payload '{"userId":"123","action":"purchase","amount":100}' --target staging-settings
```

### Invalid input (below minimum)

```bash
cre workflow simulate my-http-workflow --non-interactive --trigger-index 0 --http-payload '{"userId":"123","action":"purchase","amount":5}' --target staging-settings
```

**Expected output:**

```
Workflow compiled
2025-11-10T11:34:38Z [SIMULATION] Simulator Initialized

2025-11-10T11:34:38Z [SIMULATION] Running trigger trigger=http-trigger@1.0.0-alpha
2025-11-10T11:34:38Z [USER LOG] Processing purchase for user 123
2025-11-10T11:34:38Z [USER LOG] Amount 5 below minimum 10

Workflow Simulation Result:
 "Amount too low"

2025-11-10T11:34:38Z [SIMULATION] Execution finished signal received
2025-11-10T11:34:38Z [SIMULATION] Skipping WorkflowEngineV2
```

### Missing fields

```bash
cre workflow simulate my-http-workflow --non-interactive --trigger-index 0 --http-payload '{"userId":"123","action":"purchase"}' --target staging-settings
```

**Expected output:**

```
Workflow compiled
2025-11-10T11:37:57Z [SIMULATION] Simulator Initialized

2025-11-10T11:37:57Z [SIMULATION] Running trigger trigger=http-trigger@1.0.0-alpha
2025-11-10T11:37:57Z [USER LOG] Missing required fields

Workflow Simulation Result:
 "Error: Missing required fields (userId, action, amount)"

2025-11-10T11:37:57Z [SIMULATION] Execution finished signal received
2025-11-10T11:37:57Z [SIMULATION] Skipping WorkflowEngineV2
```

This helps you verify error handling and edge cases before deployment.

## Simulation vs production behavior

| Aspect                     | Simulation                          | Production                              |
| -------------------------- | ----------------------------------- | --------------------------------------- |
| **Authorization**          | Not required (empty config allowed) | Required (`authorizedKeys` must be set) |
| **Signature verification** | Skipped                             | Strictly enforced                       |
| **Execution**              | Immediate, synchronous              | Asynchronous via DON                    |
| **Logs**                   | Printed to terminal                 | Available in CRE UI                     |

> **CAUTION: Add authorization before deploying**
>
> Simulation allows empty authorization configs for convenience, but **deployed workflows require `authorizedKeys`**.
> The CLI will reject deployments with HTTP triggers that lack authorization configuration.

## Local listener vs the deployed gateway

If your backend triggers deployed workflows through the CRE gateway, you may want to send the same requests to the local simulator. The local HTTP server that `--listen` starts is not a copy of the gateway. Use this table to see what differs:

| Aspect                 | Local simulator (`--listen`)                                                                                   | Deployed workflow (gateway)                                                                                                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Endpoint**           | `POST http://localhost:2000/trigger`                                                                           | The [gateway URL](/cre/guides/workflow/using-triggers/http-trigger/triggering-deployed-workflows#gateway-urls) for your workflow's DON                                                       |
| **Request format**     | `{"input": { ... }}`                                                                                           | JSON-RPC `workflows.execute` request with the payload in `params.input`. See [Request format](/cre/guides/workflow/using-triggers/http-trigger/triggering-deployed-workflows#request-format) |
| **Workflow selection** | One workflow and trigger per simulator process. The request does not choose a workflow                         | The request selects the workflow, for example with `params.workflow.workflowID`                                                                                                              |
| **Authorization**      | None. The simulator does not check a JWT or `authorizedKeys`                                                   | JWT signed with a key listed in `authorizedKeys`. See [JWT authentication](/cre/guides/workflow/using-triggers/http-trigger/triggering-deployed-workflows#jwt-authentication)                |
| **Response**           | Empty `200 OK`, returned before the workflow runs. Results appear in the terminal                              | JSON-RPC response with `result.status` set to `ACCEPTED`. See [Response format](/cre/guides/workflow/using-triggers/http-trigger/triggering-deployed-workflows#response-format)              |
| **Execution**          | One run at a time. Up to 16 requests wait in a queue                                                           | Asynchronous on the DON                                                                                                                                                                      |
| **Rate limit**         | One run per 30 seconds by default. Skipped requests still return `200 OK`. Disable with `--limits none`        | Production [HTTP trigger quota](/cre/service-quotas#http-trigger)                                                                                                                            |
| **Invalid requests**   | `400 Bad Request` if the body isn't valid JSON. If `input` is missing, the workflow runs with an empty payload | JSON-RPC error response. See [Error response](/cre/guides/workflow/using-triggers/http-trigger/triggering-deployed-workflows#error-response)                                                 |

To send gateway-format requests to the simulator, unwrap `params.input` in your own code and send it as `{"input": ...}` to `/trigger`. To run several workflows locally, start one simulator per workflow, each with its own `--http-trigger-port`. To test JWT authorization, use a deployed workflow. See [Testing with Local JWT Server](/cre/guides/workflow/using-triggers/http-trigger/local-testing-tool).

> **NOTE: Two tools can use port 2000**
>
> The `cre-http-trigger` tool in [Testing with Local JWT
> Server](/cre/guides/workflow/using-triggers/http-trigger/local-testing-tool) also listens on `localhost:2000/trigger`,
> but it forwards your request to a deployed workflow. It does not run the simulator. If you run both at the same time,
> start the simulator on another port with `--http-trigger-port`.

## Next steps

Once you've tested your workflow locally:

1. **Add authorization**: Configure `authorizedKeys` in your HTTP trigger for production
2. **Deploy your workflow**: Use [`cre workflow deploy`](/cre/guides/operations/deploying-workflows) to register it
3. **Trigger it in production**: Follow the [Triggering Deployed Workflows](/cre/guides/workflow/using-triggers/http-trigger/triggering-deployed-workflows) guide