---
title: "Push and pull apps"
description: "Serve workflows from an HTTPS endpoint that EZGH calls, or run workers that poll EZGH for work."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.ezghcloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Push and pull apps

An app is one deployment of your code that hosts workflows. Its kind, set when you create it and
fixed after, decides how EZGH reaches your code:

| | Push | Pull |
| --- | --- | --- |
| How it runs | EZGH sends signed HTTPS requests to your endpoint | Your workers poll EZGH for tasks |
| Needs | A public HTTPS endpoint on port 443 | Outbound HTTPS only |
| One invocation may take | Your platform's request limit, at most 15 minutes | 15 minutes to 24 hours |
| Credential your code holds | The app's signing secret | An API key with `workflows.workers.poll` |
| How EZGH learns your workflows | A sync after each deploy | Each poll announces them |
| Good for | Serverless platforms and web apps (Vercel, Cloudflare Workers, Cloud Run, Lambda) | Code behind a firewall, long CPU-bound steps, local development |

The workflow code is the same for both; only how you serve it differs.

## Push apps

### Create the app

Create a push app with its endpoint, `https://` on port 443:

### TypeScript

```ts
import { Client } from "@ezgh/workflows"

const client = new Client({ apiKey, organizationId, projectId })
const { app, signingSecret } = await client.createApp("orders", { endpoint: "https://app.example.com/api/workflows" })
```
### Go

```go
c, err := workflows.NewClient(workflows.ClientOptions{APIKey: apiKey, OrganizationID: orgID, ProjectID: projectID})
created, err := c.CreateApp(ctx, "orders", workflows.CreateAppOptions{Endpoint: "https://app.example.com/workflows"})
// created.App, *created.SigningSecret
```
### Python

```python
from ezgh_workflows import Client

client = Client(api_key=api_key, organization_id=org_id, project_id=project_id)
created = client.create_app("orders", endpoint="https://app.example.com/ezgh-workflows/")
# created["app"], created["signingSecret"]
```

Or call [CreateApp](/workflows-api/CreateApp/) with `{"name": "orders", "kind": "push",
"endpoint": "…"}`. **The answer carries the app's signing secret** (`whsec_…`) **once:** store it
where your code can read it, for example in `EZGH_WORKFLOWS_SIGNING_SECRET`. A repeated create with
the same `Idempotency-Key` answers `signingSecret: null`; if you lost the secret, rotate it.

The app starts `unsynced`. EZGH sends nothing to an app until it has synced.

### Serve your workflows

Each SDK serves your workflows from one route, and verifies every request's `EZGH-Signature`
header against the signing secret, refusing requests more than 5 minutes old:

### TypeScript

```ts
import { serve } from "@ezgh/workflows"

// A Fetch API handler, (Request) => Promise<Response>: Bun, Node, Deno, Cloudflare Workers, Next.js
export const POST = serve({
  workflows: [onboarding],
  signingSecrets: [process.env.EZGH_WORKFLOWS_SIGNING_SECRET],
  maxInvocationSeconds: 300,
})
```
### Go

```go
h, err := workflows.Handler(workflows.HandlerOptions{
	Workflows:            []workflows.Workflow{onboarding},
	SigningSecrets:       []string{os.Getenv("EZGH_WORKFLOWS_SIGNING_SECRET")},
	MaxInvocationSeconds: 300,
})
http.Handle("/workflows", h) // a net/http handler
```
### Python

```python
from ezgh_workflows import serve

# An ASGI app: run it with uvicorn, or mount it in Starlette or FastAPI
app = serve(
    workflows=[onboarding],
    signing_secrets=[os.environ.get("EZGH_WORKFLOWS_SIGNING_SECRET")],
    max_invocation_seconds=300,
)
```

**`maxInvocationSeconds`** is how long your platform lets a request run, 1 to 900 seconds (default
300). EZGH sets each invocation's deadline inside it, and the SDK stops starting steps when less
than 10 % of the time (at least 5 seconds) is left; EZGH then invokes the workflow again. A step
can't outlast its invocation, so steps longer than your request limit need a [pull
worker](/workflows/pull-workers).

### Sync after each deploy

A sync tells EZGH your endpoint and its workflows. EZGH sends a signed sync request to the
endpoint, and the SDK answers with the workflows it serves, their retry policies and timeouts. Run
it from your deploy pipeline:

### TypeScript

```ts
await client.sync(appId, { endpoint: "https://app.example.com/api/workflows" })
```
### Go

```go
app, err := c.Sync(ctx, appID, workflows.SyncOptions{Endpoint: "https://app.example.com/workflows"})
```
### Python

```python
client.sync(app_id)
```

[SyncApp](/workflows-api/SyncApp/) takes an optional `endpoint` (else the app's) and
`deployment`. **The endpoint changes only when a sync succeeds:** a sync that fails is
`409 sync_failed` and changes nothing, so an app can't be pointed at an address that didn't
answer as an EZGH SDK with the app's secret. Runs start only for workflows the last sync
reported: starting a run of an app that never synced is `409 app_not_synced`, and of a workflow
it didn't report `409 workflow_not_synced`.

### Rotate the signing secret

[RotateAppSecret](/workflows-api/RotateAppSecret/) returns a new secret, once. The previous
secrets stay valid for 24 hours, and requests are signed with every valid secret, so deploy the new
one alongside the old (each SDK takes a list of secrets), then remove the old one.

### Endpoints EZGH calls

- HTTPS on port 443, with a publicly trusted certificate. Redirects aren't followed.
- Not IP addresses, `localhost`, or names under `.local`, `.internal`, `.test`, `.svc` or
  `.localhost`, and never an address that resolves to a private, loopback, link-local or shared
  address.
- An answer is at most 16 MiB.

### When your endpoint fails

- **A step had started** in the failed invocation: that's a failed attempt of the step, retried
  under its [retry policy](/workflows/retries).
- **No step had started** (the endpoint was unreachable, or your code crashed before its first
  step): the invocation is retried with backoff, at most 10 minutes apart, for 24 hours. Then the
  run fails with `invocation_failed`. A `429` answer's `Retry-After` is honored.
- **Every invocation of the app fails for 5 minutes:** EZGH pauses delivery to the app, marks it
  `unhealthy`, and tries a sync request once a minute. Delivery resumes once one succeeds; runs
  wait meanwhile instead of using up their retries.

## Pull apps

Create a pull app with `"kind": "pull"`, an optional task `queue` (default `default`) and
`partitions` (default 4). It has no endpoint, signing secret or sync. Workers poll its queue with
an API key and announce the workflows they serve. See [Pull workers](/workflows/pull-workers) and
the [quickstarts](/workflows/quickstart-typescript).

## Delete an app

[DeleteApp](/workflows-api/DeleteApp/) deletes the app and its schedules.

Source: https://docs.ezghcloud.com/workflows/push-and-pull/index.mdx
