# GitHub Actions for Beginners: Understanding Triggers, Conditions, Variables and Debugging Your Workflows

## Is your pipeline running only when it should?

In Part 1 we built a pipeline that lints, tests, and builds on every push. That’s a good start, but a pipeline that runs on everything isn’t really required all the times.

Getting it right means answering four questions about every workflow:

1. When should it run? That’s what **triggers** control.
2. Should this job or step run at all? That’s what **conditions** decide.
3. What values do the steps work with, and how do they share them? That’s **variables and outputs.**
4. What do I do when its not working? That’s **debugging.**

This post answers each one with working examples. By the end, you’ll have upgraded the Part 1 pipeline so it runs only when it needs to, and you’ll know how to troubleshoot if something is wrong.

## **1\. Triggers: when does a workflow run?**

Everything under the `on:` key is a trigger. A workflow can listen to one event or several.

**The events we will use most:**

| Event | Runs when |
| --- | --- |
| `push` | Commits are pushed to a branch or tag |
| `pull_request` | A PR is opened, updated, or reopened |
| `workflow_dispatch` | Someone triggers it manually |
| `schedule` | you save the slot |
| `release` | A GitHub release is created or published |
| `workflow_call` | Another workflow calls this one |
| `workflow_run` | Another workflow finishes |

There are many more, including events for issues, comments, and labels, which is what makes **Actions** useful for repository automation as well as CI.

**Filtering by branch, tag, and path:**

Without filters, `push` runs for every branch. Usually not necessary in most of the cases:

```yaml
on:
  push:
    branches: [main]
    paths-ignore:
      - '**.md'
      - 'docs/**'
  pull_request:
    branches: [main]
    paths-ignore:
      - '**.md'
      - 'docs/**'
```

*Understanding the above yaml snippet:* The workflow triggers on commit and PRs targeting main branch only, any updates to those specific files and folders mentioned under inbuilt keyword `paths-ignore` will not trigger the workflow run.

**A few rules to know:**

- You can’t use `paths` and `paths-ignore` for the same event. Pick one or use `!` with `paths`. The same goes for `branches, tags`and `branches-ignore, tags-ignore`.
- Patterns use globs. `*` matches within one folder level, and `**` matches across folders.
- Quote patterns that start with `*`, or YAML will try to read them as something else.
- Filters are case-sensitive on Linux runners.

**To trigger on release tags like** `v1.4.2`**:**

```yaml
on:
  push:
    tags:
      - 'v*.*.*'
```

**Pull request activity types:**

By default, `pull_request` runs when a PR is **opened**, **synchronized** (new commits pushed), or **reopened**. You can add more types when you need them:

```yaml
on:
  pull_request:
    types: [opened, synchronize, reopened]
```

**Running a workflow manually:**

`workflow_dispatch` adds a "Run workflow" button to the Actions tab and lets you ask for inputs:

```yaml
on:
  workflow_dispatch:
    inputs:
      environment:
        description: Where should this run?
        type: choice
        options: [staging, production]
        default: staging
      verbose:
        description: Print extra logs
        type: boolean
        default: false
```

![](https://cdn.hashnode.com/uploads/covers/6ab407e4ef329dc6a7ba7434/a165f67e-5a1e-4e34-8aff-40245749397b.png align="center")

*One catch:* the button only shows up once the workflow file exists on your **default branch**.

**Running on a schedule:**

```yaml
on:
  schedule:
    - cron: '0 6 * * 1'   # every Monday at 06:00 UTC
```

The five fields are minute, hour, day of month, month, and day of week.

## **2\. Conditions: should this job or step run?**

Triggers decide whether the workflow starts. The `if` key then decides whether an individual job or step runs. In the below case, the first `if` says execute the job only when the main branch is committed and second `if` says execute if it’s a Linux runner.

```yaml
jobs:
  deploy:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - name: Only run on Linux
        if: runner.os == 'Linux'
        run: echo "Running on Linux"
```

<mark>Job execution is skipped as it didn’t meet the defined conditions</mark>

![](https://cdn.hashnode.com/uploads/covers/6ab407e4ef329dc6a7ba7434/6fc08ce5-0508-47aa-af42-806a8b655b7c.png align="center")

<mark>Job execution is successful as the required conditions are met</mark>

![](https://cdn.hashnode.com/uploads/covers/6ab407e4ef329dc6a7ba7434/3b51244e-28b7-4b9d-90ca-1507d68ebabe.png align="center")

**Status functions:**

By default every step has an invisible `if: success()`, meaning "run only if everything before passed." You can change that:

| Function | True when |
| --- | --- |
| `success()` | All previous steps passed (the default) |
| `failure()` | Any previous step failed |
| `always()` | Always, even if the run was cancelled |
| `cancelled()` | The run was cancelled |

Here's a pattern you'll use constantly, running cleanup or a report even when the tests fail:

```yaml
steps:
  - name: Run tests
    run: npm test

  - name: Runs only if tests pass
    run: echo "All tests passed"

  - name: Runs only if something failed
    if: failure()
    run: echo "Something failed, check the logs above"

  - name: Runs no matter what
    if: always()
    run: echo "Finished, whatever the result"
```

## **3\. Variables and data: env, vars, secrets, and outputs**

There are three ways to give a workflow configuration, and they're easy to mix up.

| Mechanism | Defined | Read with | Use for |
| --- | --- | --- | --- |
| `env` | In the YAML itself | `${{ env.NAME }}` | Values that belong in the workflow file |
| `vars` | In repository, environment, or organization settings | `${{ vars.NAME }}` | Non-sensitive information |
| `secrets` | In repository, environment, or organization settings, stored encrypted | `${{ secrets.NAME }}` | Sensitive information |

`env` can be set at three levels, and the closest one wins:

```yaml
env:
  APP_ENV: ci                            # at workflow level

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      LOG_LEVEL: info                    # at job level
    steps:
      - name: Run tests
        env:
          API_URL: ${{ vars.API_URL }}   # at step level
        run: npm test
```

**GitHub supports both default variables as mentioned below and custom variables (using env and var):**

```yaml
- name: Show run info
  run: |
    echo "Commit:  $GITHUB_SHA"
    echo "Branch:  $GITHUB_REF_NAME"
    echo "Actor:   $GITHUB_ACTOR"
    echo "Run #:   $GITHUB_RUN_NUMBER"
    echo "Runner:  $RUNNER_OS"
    echo "CI flag: $CI"
```

**Passing data between steps and jobs:**

In certain scenarios you may need the output from one step to be passed onto another in the same job or across different jobs, let’s see how we can achieve it.

*Across steps in the same job:* For every run, a step starts in a fresh shell. A variable you set in one step is gone by the next:

```yaml
steps:
  - name: Step one
    run: name=Adithya Meda

  - name: Step two
    run: echo "Hello $name"    # prints "Hello " because the variable is empty
```

![](https://cdn.hashnode.com/uploads/covers/6ab407e4ef329dc6a7ba7434/818b8e05-e7cc-427a-9c79-73dc254a0099.png align="center")

To pass a value to a later step, write it to a default key `$GITHUB_OUTPUT`, then read it back by the step’s id:

```yaml
steps:
  - name: Step one
    id: one
    run: echo "name=Adithya Meda" >> "$GITHUB_OUTPUT"

  - name: Step two
    run: echo "Hello ${{ steps.one.outputs.name }}"   # prints "Hello AdithyaMeda"
```

![](https://cdn.hashnode.com/uploads/covers/6ab407e4ef329dc6a7ba7434/0bf3c82d-61b2-4f90-89c3-3bcf8678cd94.png align="center")

*How to define:*

1. Give the first step an `id`.
2. Write `<output name>=<value>` to `$GITHUB_OUTPUT`.
3. Read it later with `steps.<id>.outputs.<output name>`.

*Across different jobs:*

```yaml
jobs:
  job-one:
    runs-on: ubuntu-latest
    outputs:
      name: ${{ steps.one.outputs.name }}
    steps:
      - id: one
        run: echo "name=AdithyaMeda" >> "$GITHUB_OUTPUT"

  job-two:
    needs: job-one
    runs-on: ubuntu-latest
    steps:
      - run: echo "Hello ${{ needs.job-one.outputs.name }}"
```

![](https://cdn.hashnode.com/uploads/covers/6ab407e4ef329dc6a7ba7434/4c9a00d4-6699-4752-b283-da4e455eb693.png align="center")

The key difference to notice is that when passing output within the same job we use `steps.` and across the jobs, we use `needs.`

**Defaults, timeouts, and failure handling:**

A few settings that make workflows more predictable:

```yaml
defaults:
  run:
    shell: bash
    working-directory: ./app

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Experimental check
        continue-on-error: true
        run: npm run experimental-check
```

- `timeout-minutes` matters more than it looks. The default job timeout is 360 minutes, so a hung process can quietly burn six hours of runner time. Set something realistic.
- `continue-on-error: true` lets a step or job fail without failing the run. Good for experimental checks, but not for production systems.
- `defaults` keyword defines fallback execution settings (like shell or working-directory) at the workflow or job level.

## **4\. Debugging: when something goes wrong**

**Why isn’t my workflow running?**

- **Wrong location or extension.** It must live in `.github/workflows/` and end in `.yml` or `.yaml`.
- **Invalid YAML.** Syntax errors in the workflow files. One bad indent is enough.
- **Filters excluded it.** Check `branches`, `tags`, and `paths` against what you actually pushed.
- **Default branch rule.** `schedule` and the manual "Run workflow" button need the file on the default branch.
- **A commit message skipped it.** `[skip ci]` or `[ci skip]` in a commit message skips `push` and `pull_request` workflows for that commit.
- **Actions is disabled** for the repository until the owner enables it.

**Some debugging actions to consider:**

1. ***Start with the logs:*** If everything is fine and your workflow runs and fails due to other reasons, open the failed run, click the failed job, and look for the red step. Each step is collapsible, and you can search within the log using the search box at the top.
2. ***Turn on debug logging:*** When the normal log isn’t enough, do this: On a failed run, click **Re-run failed jobs** and tick **Enable debug logging**. This is the easiest route or set `ACTIONS_STEP_DEBUG` to `true` as a repository secret or variable to get verbose step output on every run. `ACTIONS_RUNNER_DEBUG` does the same for runner diagnostics.
3. ***Make your own logs readable:*** Workflow commands let you format output and raise annotations that show up directly on the run and the pull request:

```yaml
- name: Check things
  run: |
    echo "::group::Environment details"
    node --version
    npm --version
    echo "::endgroup::"
    echo "::notice::Using Node $(node --version)"
    echo "::warning file=src/app.js,line=12::Consider validating this input"
    echo "::error file=src/app.js,line=20::This will break on Windows"
```

`::group::` collapses noisy output, and `::error`, `::warning`, and `::notice` appear as annotations. Adding `file` and `line` pins them to a specific line in the diff.

### **Common symptoms and likely causes:**

| Symptom | Likely cause |
| --- | --- |
| Workflow never appears | Wrong folder, invalid YAML, or not on the default branch |
| A job is "skipped" and a dependent job too | A `needs` job was skipped or failed, or the `if` was false |
| Step passes locally, fails in CI | Different Node or OS version, missing env var, or a missing/wrong commands |
| `Resource not accessible by integration` | The `GITHUB_TOKEN` lacks a permission (Part 3) |
| A `paths` filter blocks a required check | The workflow was skipped, so the check never reported |

Most broken pipelines come from small mistakes, like a filter that’s not properly applied or a value that never reached the next step.

### **What's next in the series:**

Part 1: GitHub Actions fundamentals and your first pipeline.

Part 2 (this post): Workflows in depth: triggers, conditions, environment variables, and debugging.

Part 3: Secrets, permissions, and securing your pipelines.

Part 4: Deployments: environments, approvals, and shipping to the cloud.

Part 5: Advanced patterns: reusable workflows, composite actions, and self-hosted runners.

Find all workflows used in this series on my [<u>GitHub Actions: Zero to Production</u>](https://github.com/Adithya-Meda/GitHub-Actions-Zero-to-Production) repository.
