# Multi-App Coordination

The Application SDK treats each `App` as an independent unit of execution — one Temporal workflow per app. When a connector needs multiple phases (metadata extraction → asset publishing, for example), those phases can run as separate apps coordinated by the Atlan platform.

---

## The Two Coordination Patterns

### Pattern 1: Automation Engine DAG (recommended)

The Atlan **Automation Engine** (AE) reads `app/generated/manifest.json` and orchestrates apps as a directed acyclic graph. The SDK generates this manifest from your PKL contract.

```
AE reads manifest.json
  → starts App A (extract)
      ↓ on success
  → starts App B (publish) with App A's connection_qualified_name as input
```

This is the recommended pattern for production multi-phase connectors. It:

- Decouples phase retries (a failed publish doesn't re-run extraction)
- Shows each phase as a separate workflow in the Temporal UI
- Lets you deploy phases at different scales or with different Temporal queues
- Requires no SDK-level coordination code — the DAG lives in `manifest.json`

The manifest is generated by the `/contract` skill from your `app.pkl`. See the [PKL contracts guide](pkl-contracts.md) for details on how to structure `app.pkl` and trigger manifest generation.

A two-phase manifest looks like:

```json
{
  "execution_mode": "native",
  "init_endpoint": "/workflows/v1/auth",
  "dag": {
    "extract": {
      "activity_name": "extract",
      "activity_display_name": "Extract Metadata",
      "app_name": "my-connector",
      "inputs": {
        "workflow_type": "extract_metadata",
        "task_queue": "atlan-my-connector-{deployment-name}",
        "args": {
          "connection_qualified_name": "{{connection}}",
          "credential_guid": "{{credential}}"
        }
      }
    },
    "publish": {
      "activity_name": "publish",
      "activity_display_name": "Publish Assets",
      "app_name": "atlan-loader",
      "inputs": {
        "workflow_type": "load",
        "task_queue": "atlan-loader-{deployment-name}",
        "args": {
          "output_path": "$.extract.outputs.output_path"
        }
      },
      "depends_on": { "node_id": "extract", "tag": "success" }
    }
  }
}
```

> **Deprecation note:** The `workflow_type` body field is a deprecated fallback — the SDK logs a deprecation warning when it encounters it. The canonical form is `POST /workflows/v1/start?entrypoint=<name>`. AE manifests currently emit `workflow_type` for compatibility with older AE controllers; once your AE controller supports query parameters on start requests, migrate to `?entrypoint=<name>`. New code outside of AE manifests should always use the query param.

The `$.extract.outputs.output_path` JSONPath reference is resolved by AE at runtime against App A's `run()` return value.

---

### Pattern 2: Single App with Multiple Entry Points

If your phases must share in-flight state or run within the same Temporal workflow history, use a single `App` with multiple `@entrypoint` methods:

```python
from application_sdk.app import App, entrypoint, task

class MyConnector(App):
    @entrypoint
    async def extract_metadata(self, input: ExtractionInput) -> ExtractionOutput:
        out = await self.fetch_schemas(FetchInput(connection_id=input.connection_id))
        return await self.transform(TransformInput(schemas=out.schemas))

    @entrypoint
    async def mine_queries(self, input: QueryInput) -> QueryOutput:
        return await self.fetch_queries(input)
```

Each `@entrypoint` is a separate Temporal workflow (`my-connector:extract-metadata`, `my-connector:mine-queries`). They can be triggered independently via `POST /workflows/v1/start?entrypoint=extract-metadata`.

This pattern works well when both entry points share the same credentials, handler, and configuration, but need to be triggered at different times or frequencies.

---

## Restarting the Same App (Incremental Processing)

For Apps that process data in pages or checkpoints and need to avoid Temporal history-size limits, `App.continue_with(new_input)` restarts the current App with a fresh workflow run while preserving correlation context. This is a within-app pattern — not cross-app coordination — but it is relevant when a single App drives a long multi-cycle extraction.

See [Apps — `continue_with()`](../concepts/apps.md#app-lifecycle) for usage details.

---

## Passing Data Between Apps

When apps coordinate via AE, the downstream app receives inputs via the `args` block in `manifest.json`. Use JSONPath references (`$.upstream_node.outputs.field`) to pass structured data.

For large intermediate files, use `FileReference` with `StorageTier.RETAINED`:

```python
from application_sdk.contracts import FileReference, Output, StorageTier, UploadInput

class ExtractionOutput(Output):
    parquet_file: FileReference   # StorageTier.RETAINED — kept for downstream

class MyExtractor(App):
    @task
    async def transform(self, input: TransformInput) -> ExtractionOutput:
        path = "/tmp/output.parquet"
        write_parquet(path, records)
        up = await self.upload(UploadInput(local_path=path))
        return ExtractionOutput(parquet_file=up.ref)
```

The downstream app receives the `FileReference.storage_path` (object-store path) via the AE args substitution and downloads it before processing.

When `atlan-objectstore` is bound, `App.upload()` writes to Atlan's S3 so downstream AE nodes (publish, lineage) can read it. Without the explicit `self.upload()` call, downstream nodes see nothing — the DAG succeeds but the publish app finds no artifact in its bucket. See [ADR-0014](../adr/0014-two-store-storage-architecture.md).

---

## Observability Across Apps

Correlation IDs propagate automatically when AE starts child apps. All spans from a multi-app run share the same `correlation_id` in logs and traces, making it easy to trace a full extraction + publish cycle in Grafana/Loki.
