{
  "slug": "code/app-execution-airflow",
  "title": "App execution with Airflow",
  "description": "Run Cuitty Code Apps through Airflow DAGs and reconcile callbacks.",
  "url": "https://cuitty.com/docs/code/app-execution-airflow",
  "markdown_url": "https://cuitty.com/docs/code/app-execution-airflow.md",
  "json_url": "https://cuitty.com/docs/code/app-execution-airflow.json",
  "frontmatter": {
    "title": "App execution with Airflow",
    "description": "Run Cuitty Code Apps through Airflow DAGs and reconcile callbacks.",
    "order": 16,
    "section": "Code",
    "updatedAt": "2026-05-24"
  },
  "headings": [
    {
      "depth": 2,
      "slug": "required-services",
      "text": "Required services"
    },
    {
      "depth": 2,
      "slug": "environment-variables",
      "text": "Environment variables"
    },
    {
      "depth": 2,
      "slug": "dag-run-payload",
      "text": "DAG run payload"
    },
    {
      "depth": 2,
      "slug": "callback-signing-rules",
      "text": "Callback signing rules"
    },
    {
      "depth": 2,
      "slug": "reconciliation-behavior",
      "text": "Reconciliation behavior"
    },
    {
      "depth": 2,
      "slug": "permissions-model",
      "text": "Permissions model"
    },
    {
      "depth": 2,
      "slug": "failure-modes-and-recovery",
      "text": "Failure modes and recovery"
    },
    {
      "depth": 2,
      "slug": "related-pages",
      "text": "Related pages"
    }
  ],
  "body_markdown": "Production Code App runs are submitted to Airflow. Cuitty Git owns app metadata, installs, grants, and run state; Airflow owns task orchestration and execution logs.\n\n## Required services\n\n- Cuitty Git API and database.\n- Airflow webserver and scheduler.\n- Cuitty Auth for actor identity.\n- SpiceDB for app install, run, and log-view permissions.\n- Optional object storage for app logs and execution artifacts.\n\n## Environment variables\n\n```bash\nAIRFLOW_URL=http://localhost:8080\nAIRFLOW_USERNAME=airflow\nAIRFLOW_PASSWORD=airflow\nCUITTY_PUBLIC_URL=http://localhost:4350\nCUITTY_APP_EXECUTION_MODE=airflow\nCUITTY_APP_RUN_CALLBACK_SECRET=dev-callback-secret\n```\n\nFor local development without a live Airflow deployment, use simulation mode:\n\n```bash\nCUITTY_APP_EXECUTION_MODE=simulate\n```\n\nWhen the auth issuer or Airflow URL points at local test hosts, Cuitty Git may also simulate Airflow automatically.\n\n## DAG run payload\n\nAirflow receives a DAG run `conf` payload shaped like this:\n\n```json\n{\n  \"run_id\": \"app-run-uuid\",\n  \"run_kind\": \"manual\",\n  \"app_id\": \"app-uuid\",\n  \"app_version_id\": \"app-version-uuid\",\n  \"installation_id\": \"install-uuid\",\n  \"actor_user_id\": \"user-uuid\",\n  \"owner\": { \"kind\": \"org\", \"slug\": \"acme\" },\n  \"input\": { \"event_target\": \"airflow:deploy_guard.pr_opened\" },\n  \"callback\": {\n    \"url\": \"http://localhost:4350/api/v1/apps/runs/app-run-uuid/callback\",\n    \"hmac_header\": \"x-cuitty-signature\",\n    \"timestamp_header\": \"x-cuitty-timestamp\"\n  }\n}\n```\n\nThe DAG ID can be provided in run input as `airflow_dag_id`, derived from an `event_target` that starts with `airflow:`, or derived from the app and run kind.\n\n## Callback signing rules\n\nAirflow should call the callback URL when a run changes state. The request must include:\n\n- `x-cuitty-timestamp` as a Unix timestamp.\n- `x-cuitty-signature` as an HMAC SHA-256 signature.\n- A body containing the Cuitty app run ID, Airflow DAG ID, Airflow run ID, state, optional log URL, and optional error message.\n\nThe signature is computed over `timestamp.body` with `CUITTY_APP_RUN_CALLBACK_SECRET`. Timestamps outside the accepted skew window are rejected, and terminal callbacks should be idempotent.\n\n## Reconciliation behavior\n\nCallbacks are not the only source of truth. Cuitty Git should reconcile submitted and running app runs by polling Airflow for DAG run state. Missed callbacks, transient API failures, and worker restarts should eventually converge to `succeeded`, `failed`, or `canceled`.\n\nTrigger reconciliation through the API when needed:\n\n```bash\ncurl -X POST http://localhost:4351/api/v1/apps/runs/reconcile   -H \"Authorization: Bearer $CUITTY_TOKEN\"\n```\n\n## Permissions model\n\nUsers must be allowed to view the app installation to see run state. Managing, running, canceling, or configuring app runs requires installation-level permissions. Airflow log URLs should be displayed only when the current user can view the run and the Airflow deployment exposes the log securely.\n\n## Failure modes and recovery\n\n- DAG submission fails: record the error and leave the app run failed or retryable.\n- API accepts a run but loses the Airflow response: reconciliation should find the deterministic DAG run or fail after timeout.\n- Callback signature fails: reject the callback, check the shared secret, and rely on reconciliation.\n- Run stuck in `submitted` or `running`: poll Airflow, inspect task logs, and force reconciliation.\n- Cancellation requested: map to Airflow cancellation when supported, otherwise keep local state cancel-requested until reconciliation completes.\n\n## Related pages\n\n- [Cuitty Code Apps](/docs/code/apps)\n- [Code App Market](/docs/code/app-market)\n- [Authorization with SpiceDB](/docs/code/authz-spicedb)\n- [Operator runbook](/docs/code/operator-runbook)",
  "body_html": "<p>Production Code App runs are submitted to Airflow. Cuitty Git owns app metadata, installs, grants, and run state; Airflow owns task orchestration and execution logs.</p>\n<h2 id=\"required-services\">Required services</h2>\n<ul>\n<li>Cuitty Git API and database.</li>\n<li>Airflow webserver and scheduler.</li>\n<li>Cuitty Auth for actor identity.</li>\n<li>SpiceDB for app install, run, and log-view permissions.</li>\n<li>Optional object storage for app logs and execution artifacts.</li>\n</ul>\n<h2 id=\"environment-variables\">Environment variables</h2>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"bash\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">AIRFLOW_URL</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">http://localhost:8080</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">AIRFLOW_USERNAME</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">airflow</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">AIRFLOW_PASSWORD</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">airflow</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">CUITTY_PUBLIC_URL</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">http://localhost:4350</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">CUITTY_APP_EXECUTION_MODE</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">airflow</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">CUITTY_APP_RUN_CALLBACK_SECRET</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">dev-callback-secret</span></span></code></pre>\n<p>For local development without a live Airflow deployment, use simulation mode:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"bash\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">CUITTY_APP_EXECUTION_MODE</span><span style=\"color:#F97583\">=</span><span style=\"color:#9ECBFF\">simulate</span></span></code></pre>\n<p>When the auth issuer or Airflow URL points at local test hosts, Cuitty Git may also simulate Airflow automatically.</p>\n<h2 id=\"dag-run-payload\">DAG run payload</h2>\n<p>Airflow receives a DAG run <code>conf</code> payload shaped like this:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"json\"><code><span class=\"line\"><span style=\"color:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"run_id\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"app-run-uuid\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"run_kind\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"manual\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"app_id\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"app-uuid\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"app_version_id\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"app-version-uuid\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"installation_id\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"install-uuid\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"actor_user_id\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"user-uuid\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"owner\"</span><span style=\"color:#E1E4E8\">: { </span><span style=\"color:#79B8FF\">\"kind\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"org\"</span><span style=\"color:#E1E4E8\">, </span><span style=\"color:#79B8FF\">\"slug\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"acme\"</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"input\"</span><span style=\"color:#E1E4E8\">: { </span><span style=\"color:#79B8FF\">\"event_target\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"airflow:deploy_guard.pr_opened\"</span><span style=\"color:#E1E4E8\"> },</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">  \"callback\"</span><span style=\"color:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">    \"url\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"http://localhost:4350/api/v1/apps/runs/app-run-uuid/callback\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">    \"hmac_header\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"x-cuitty-signature\"</span><span style=\"color:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#79B8FF\">    \"timestamp_header\"</span><span style=\"color:#E1E4E8\">: </span><span style=\"color:#9ECBFF\">\"x-cuitty-timestamp\"</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}</span></span></code></pre>\n<p>The DAG ID can be provided in run input as <code>airflow_dag_id</code>, derived from an <code>event_target</code> that starts with <code>airflow:</code>, or derived from the app and run kind.</p>\n<h2 id=\"callback-signing-rules\">Callback signing rules</h2>\n<p>Airflow should call the callback URL when a run changes state. The request must include:</p>\n<ul>\n<li><code>x-cuitty-timestamp</code> as a Unix timestamp.</li>\n<li><code>x-cuitty-signature</code> as an HMAC SHA-256 signature.</li>\n<li>A body containing the Cuitty app run ID, Airflow DAG ID, Airflow run ID, state, optional log URL, and optional error message.</li>\n</ul>\n<p>The signature is computed over <code>timestamp.body</code> with <code>CUITTY_APP_RUN_CALLBACK_SECRET</code>. Timestamps outside the accepted skew window are rejected, and terminal callbacks should be idempotent.</p>\n<h2 id=\"reconciliation-behavior\">Reconciliation behavior</h2>\n<p>Callbacks are not the only source of truth. Cuitty Git should reconcile submitted and running app runs by polling Airflow for DAG run state. Missed callbacks, transient API failures, and worker restarts should eventually converge to <code>succeeded</code>, <code>failed</code>, or <code>canceled</code>.</p>\n<p>Trigger reconciliation through the API when needed:</p>\n<pre class=\"astro-code github-dark\" style=\"background-color:#24292e;color:#e1e4e8; overflow-x: auto;\" tabindex=\"0\" data-language=\"bash\"><code><span class=\"line\"><span style=\"color:#B392F0\">curl</span><span style=\"color:#79B8FF\"> -X</span><span style=\"color:#9ECBFF\"> POST</span><span style=\"color:#9ECBFF\"> http://localhost:4351/api/v1/apps/runs/reconcile</span><span style=\"color:#79B8FF\">   -H</span><span style=\"color:#9ECBFF\"> \"Authorization: Bearer </span><span style=\"color:#E1E4E8\">$CUITTY_TOKEN</span><span style=\"color:#9ECBFF\">\"</span></span></code></pre>\n<h2 id=\"permissions-model\">Permissions model</h2>\n<p>Users must be allowed to view the app installation to see run state. Managing, running, canceling, or configuring app runs requires installation-level permissions. Airflow log URLs should be displayed only when the current user can view the run and the Airflow deployment exposes the log securely.</p>\n<h2 id=\"failure-modes-and-recovery\">Failure modes and recovery</h2>\n<ul>\n<li>DAG submission fails: record the error and leave the app run failed or retryable.</li>\n<li>API accepts a run but loses the Airflow response: reconciliation should find the deterministic DAG run or fail after timeout.</li>\n<li>Callback signature fails: reject the callback, check the shared secret, and rely on reconciliation.</li>\n<li>Run stuck in <code>submitted</code> or <code>running</code>: poll Airflow, inspect task logs, and force reconciliation.</li>\n<li>Cancellation requested: map to Airflow cancellation when supported, otherwise keep local state cancel-requested until reconciliation completes.</li>\n</ul>\n<h2 id=\"related-pages\">Related pages</h2>\n<ul>\n<li><a href=\"/docs/code/apps\">Cuitty Code Apps</a></li>\n<li><a href=\"/docs/code/app-market\">Code App Market</a></li>\n<li><a href=\"/docs/code/authz-spicedb\">Authorization with SpiceDB</a></li>\n<li><a href=\"/docs/code/operator-runbook\">Operator runbook</a></li>\n</ul>",
  "links_out": [
    "/docs/code/apps",
    "/docs/code/app-market",
    "/docs/code/authz-spicedb",
    "/docs/code/operator-runbook"
  ]
}