← All examples
DevOpsApprovalModules

Put a durable safety gate in front of production deployments

Accept a release from CI, run independent preflight checks, require approval only for production, deploy through your own infrastructure module, verify health, and roll back on failure.

WOML does not need a built-in connector for every cloud. A local TypeScript module can wrap your deployment API while Rust still supervises the workflow around it.

The workflow at a glance

Text
CI webhook
  → [Verify artifact ∥ Check current service]
  → Production? ─ yes → Operations approval
                └ no  → Automatic authorization
  → Deploy
  → Verify health
  → Healthy? ─ yes → Complete
             └ no  → Roll back

The infrastructure module

Save as deployment-api.ts:

TypeScript
export async function deploy(
  service: string,
  image: string,
  environment: string,
  token: string,
) {
  const response = await services.http.request({
    method: "POST",
    url: `https://deploy.example.com/environments/${environment}/releases`,
    headers: { authorization: `Bearer ${token}` },
    json: { service, image },
    timeout: "2m",
  }, { name: "deploy-release" });

  return response.data;
}

export async function rollback(
  environment: string,
  deploymentId: string,
  token: string,
) {
  const response = await services.http.request({
    method: "POST",
    url: `https://deploy.example.com/environments/${environment}/rollbacks`,
    headers: { authorization: `Bearer ${token}` },
    json: { deploymentId },
    timeout: "2m",
  }, { name: "rollback-release" });

  return response.data;
}

The workflow

WOML
<woml>
  <imports>
    <module name="deploymentApi" from="./deployment-api.ts" />
  </imports>

  <workflow
    id="deployment-safety-gate"
    name="Deployment safety gate"
    description="Authorize, deploy, verify, and roll back application releases."
    version="1.0.0"
  >
    <config concurrency="4" timeout="2h" queue="deployments" />

    <triggers>
      <webhook
        id="releaseCandidate"
        path="/webhooks/releases"
        method="POST"
        auth="bearer"
        secret="{{secrets.DEPLOY_WEBHOOK_TOKEN}}"
      >
        <schema>
          {
            "type": "object",
            "required": ["releaseId", "service", "image", "environment", "healthUrl", "artifactUrl"],
            "properties": {
              "releaseId": { "type": "string" },
              "service": { "type": "string" },
              "image": { "type": "string" },
              "environment": { "type": "string", "enum": ["staging", "production"] },
              "healthUrl": { "type": "string" },
              "artifactUrl": { "type": "string" }
            }
          }
        </schema>
      </webhook>
    </triggers>

    <steps>
      <parallel id="preflight" name="Run deployment preflight" concurrency="2" on-error="wait-all">
        <step id="artifact" name="Verify release artifact" retry="3">
          <script>
            const response = await services.http.request({
              method: "HEAD",
              url: context.payload.artifactUrl,
              timeout: "15s"
            }, { name: "verify-release-artifact" });
            return { available: response.status >= 200 && response.status < 300 };
          </script>
        </step>

        <step id="currentService" name="Check current service">
          <script>
            const response = await services.http.request({
              url: context.payload.healthUrl,
              timeout: "10s",
              acceptedStatus: { minimum: 100, maximum: 599 }
            }, { name: "check-current-service" });
            return { status: response.status, healthy: response.ok };
          </script>
        </step>
      </parallel>

      <step id="policy" name="Evaluate release policy">
        <script>
          return {
            production: context.payload.environment === "production",
            preflightPassed: context.steps.artifact.available && context.steps.currentService.healthy
          };
        </script>
      </step>

      <choose id="authorization" name="Authorize release">
        <when test="{{context.steps.policy.production}}">
          <approval id="productionApproval" name="Approve production deployment" timeout="2h" on-timeout="reject">
            <notify>
              <slack
                channels="#deployments"
                bot-token="{{secrets.SLACK_BOT_TOKEN}}"
                app-token="{{secrets.SLACK_APP_TOKEN}}"
              />
            </notify>
            <when-approved><step id="authorizeProduction"><script>return { approved: true, source: "operator" };</script></step></when-approved>
            <when-rejected><step id="rejectProduction"><script>return { approved: false, source: "operator" };</script></step></when-rejected>
          </approval>
          <result value="{{context.steps.productionApproval}}" />
        </when>
        <otherwise>
          <step id="authorizeStaging"><script>return { decision: "approved", source: "staging-policy" };</script></step>
          <result value="{{context.steps.authorizeStaging}}" />
        </otherwise>
      </choose>

      <step id="deploy" name="Deploy release" retry="3" retry-backoff="exponential">
        <script>
          if (!context.steps.policy.preflightPassed || context.steps.authorization.decision !== "approved") {
            return { deployed: false, reason: "not-authorized" };
          }

          const deployment = await services.deploymentApi.deploy(
            context.payload.service,
            context.payload.image,
            context.payload.environment,
            secrets.DEPLOY_API_TOKEN
          );

          return { deployed: true, deploymentId: deployment.id };
        </script>
      </step>

      <step id="verify" name="Verify deployed release">
        <script>
          if (!context.steps.deploy.deployed) return { healthy: false };

          const response = await services.http.request({
            url: context.payload.healthUrl,
            timeout: "30s",
            acceptedStatus: { minimum: 100, maximum: 599 }
          }, { name: "verify-deployed-service" });

          return { healthy: response.ok, status: response.status };
        </script>
      </step>

      <choose id="outcome" name="Keep or roll back release">
        <when test="{{context.steps.verify.healthy}}">
          <step id="keepRelease"><script>return { status: "deployed", deploymentId: context.steps.deploy.deploymentId };</script></step>
          <result value="{{context.steps.keepRelease}}" />
        </when>
        <otherwise>
          <step id="rollbackRelease" name="Roll back failed release">
            <script>
              if (!context.steps.deploy.deploymentId) return { status: "not-deployed" };
              await services.deploymentApi.rollback(
                context.payload.environment,
                context.steps.deploy.deploymentId,
                secrets.DEPLOY_API_TOKEN
              );
              return { status: "rolled-back", deploymentId: context.steps.deploy.deploymentId };
            </script>
          </step>
          <result value="{{context.steps.rollbackRelease}}" />
        </otherwise>
      </choose>
    </steps>
  </workflow>
</woml>

Configure and run it

Terminal
woml secrets set DEPLOY_WEBHOOK_TOKEN
woml secrets set DEPLOY_API_TOKEN
woml secrets set SLACK_BOT_TOKEN
woml secrets set SLACK_APP_TOKEN
woml check deployment-safety-gate.woml
woml run deployment-safety-gate.woml

Replace the deployment API URLs with the API for Kubernetes, Nomad, your cloud, or your internal platform.

Why this shows WOML's range

The workflow is executable release policy. It combines arbitrary infrastructure code with parallel checks, a production-only human gate, retryable managed operations, post-deployment verification, and explicit rollback—all versioned beside the application instead of trapped in a CI or visual automation UI.