Sending Project Todo Items into Your Discord Channel

The following article provides both the working Github actions and the recommended Secrets to iterate over the open projects and send those To Do items over to a Discord Channel.

As a company we make daily use of Discord in order to co-ordinate brief conversations and updates and whilst there are many solutions to project management and development the reality is your developers are working local to issues within the repository; so why add extra steps to get to the same outcomes ?

Using a workflow to send Projects Todos into Discord

To use this workflow you will require a Personal Action Token whose scope is set to allow Read access to projects and repositories. The User in question should already be able to see project listed in those Repositories within the Organiztion.

**We found working with a Personal Access Token from within the Organisation was an effective way of keeping all the projects in one view **

You will also need to create a Channel Webhook within Discord in order for your Github Action to post the content over to the channel.

Differences between the object structure of issues and draft issues

Whilst assembling this project we noticed that whilst Items was returning values these were more often associated with active Issues ; we had to modify some of the returned filters in order to access content located as Drafts; which is detailed within the second query in the workflow gets nodes containing fieldValues and content. The fieldValues nodes contain ProjectV2ItemFieldTextValue, the text value of which was used to get the title for the Issues. Draft issues titles were pulled from the item.content.title. The map that does this tries to find this title value first, if it does not exist then it will try to find the title field in item.fieldValues.nodes. A fallback "Draft" is in place in case there is an issue that does not include a title anywhere in the object.

  • Issues: Title is extracted from item.content.title.
  • Draft Issues: Title is pulled from item.fieldValues.nodes if not present in item.content.title.
  • Fallback: If no title exists, "Draft" is used.

GH_TOKEN Secret

The workflow included the environment variable github-token, which is set with secrets.GHTOKEN. It was previously set to GITHUB_TOKEN, but according to https://docs.github.com/en/codespaces/managing-codespaces-for-your-organization/managing-development-environment-secrets-for-your-repository-or-organization#naming-secrets, secrets must not be prefixed with "GITHUB". If you encounter an error message which seems vague or unclear we recommend double checking to ensure your GH_TOKEN is configured. It would be ideal if github actions reported on issues with secrets that are being requested but not set within the environment.

Adding Secrets to a GitHub Repository

  1. Go to your repository in GitHub.
  2. Open Settings.
  3. Select Secrets and variables > Actions.
  4. Click New repository secret.
  5. Enter the secret name and value (e.g., GH_TOKEN, WEBHOOK_DISCORD_CLOUDFLARE_PROJECTS).
  6. Click Add secret.

Secrets can be referenced in workflows as ${{ secrets.SECRET_NAME }} and are not exposed in logs.


list_projects.yml

name: Send Projects Todos into Discord

on:
  workflow_dispatch:
  schedule:
    - cron: '30 8 * * 1-5'
    - cron: '0 14 * * 1-5'

jobs:
  sendtodos:
    runs-on: ubuntu-latest
    permissions:
      # These permissions are required to access project and issue data
      contents: read
      issues: read
      repository-projects: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Get TODOs from GitHub Projects
        id: get_todos
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GH_TOKEN }}
          script: |
            // This script retrieves all "To Do" items from GitHub Projects v2
            // associated with the repository and formats them into a JSON object.
            const owner = context.repo.owner;
            const repo = context.repo.repo;
            let projectItems = {};

            // 1. First, get the project IDs and the IDs for their "Status" fields and "To Do" options.
            console.log(owner, repo)

            const projectsQuery = `
              query($owner: String!) {
                organization(login: $owner ) {
                  projectsV2(first: 100) {
                    nodes {
                      id
                      title
                      fields(first: 100) {
                        nodes {
                          ... on ProjectV2SingleSelectField {
                            id
                            name
                            options {
                              id
                              name
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            `;

            const variables = {
              owner: context.repo.owner,
              name: context.repo.repo
            }


            const response = await github.graphql(projectsQuery, variables);
            const allProjects = response.organization.projectsV2.nodes;

            // 2. Iterate through each project to find the "To Do" items.
            for (const project of allProjects) {
              // Find the 'Status' field
              const statusField = project.fields.nodes.find(field => field.name === 'Status');
              if (!statusField) continue;

              // Find the 'To Do' option within the 'Status' field
              const todoOption = statusField.options.find(option => option.name === 'Todo');
              if (!todoOption) continue;

              // 3. Query for items filtered by the 'To Do' status.
              const projectItemsQuery = `
                query($projectId: ID!) {
                  node(id: $projectId ) {
                    ... on ProjectV2 {
                      url
                      items(first: 100, orderBy: {field: POSITION, direction: ASC}) {
                        nodes{
                          id
                          fieldValues(first: 80) {
                            nodes{
                              ... on ProjectV2ItemFieldTextValue {
                                text
                                field {
                                  ... on ProjectV2FieldCommon {
                                    name
                                  }
                                }
                              }
                              ... on ProjectV2ItemFieldDateValue {
                                date
                                field {
                                  ... on ProjectV2FieldCommon {
                                    name
                                  }
                                }
                              }
                              ... on ProjectV2ItemFieldSingleSelectValue {
                                name
                                field {
                                  ... on ProjectV2FieldCommon {
                                    name
                                  }
                                }
                              }
                            }
                          }
                          content{
                            ... on DraftIssue {
                              title
                              body
                            }
                            ...on Issue {
                              title
                              url
                            }
                            ...on PullRequest {
                              title
                              url
                              assignees(first: 100) {
                                nodes{
                                  login
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              `;

              const itemsVariables = {
                projectId: project.id,
                statusFieldId: statusField.id,
                todoOptionId: todoOption.id
              }

              const { node } = await github.graphql(projectItemsQuery, itemsVariables);

              if (node) {
                projectItems[project.title] = {
                  url: node.url,
                  items: node.items.nodes
                    .filter(item => item)
                }
              }
            }

            let splitData = {}

            for (const [project, items] of Object.entries(projectItems)) {
              const projectUrl = items.url
              const todos = items.items
                .filter(item => item.fieldValues.nodes.some(
                  node => node.name === "Todo" || (node.field && node.field.name === "Status" && node.name === "Todo")
                ))
                .map(item => ({
                  title: item?.content?.title ?? item?.fieldValues?.nodes?.find(n => n.field?.name?.toLowerCase() === "title")?.text ?? "Draft",
                  url: item?.content?.url ?? items.url
                }))

              if (todos) {
                splitData[project] = {
                  url: projectUrl,
                  todos: todos
                }
              }
            }

            // Export the formatted JSON to a step output for the next step to use.
            const output = splitData
            core.setOutput('todo_json', output);

      - name: Send TODO list to Discord
        if: success() && steps.get_todos.outputs.todo_json != '{}'
        env:
          WEBHOOK_DISCORD_CLOUDFLARE_PROJECTS: ${{ secrets.WEBHOOK_DISCORD_CLOUDFLARE_PROJECTS }}
          TODO_JSON: ${{ steps.get_todos.outputs.todo_json }}
        run: |
          # Use jq to create a rich JSON payload with embeds for Discord.
          DISCORD_PAYLOAD=$(jq -n \
            --arg content "Daily Project TODOs Check-in" \
            --arg todos "$TODO_JSON" \
            '{
              "content": $content,
              "embeds": [
                {
                  "title": "Current TODOs",
                  "description": "Here is a list of all items in the To Do column across your repository projects:",
                  "color": 3447003, # A nice Discord blue
                  "fields": ($todos | fromjson | to_entries | map({
                    name: ("[" + .key + "](" + .value.url + ")"),
                    value: (.value.todos | map("• " + .title) | join("\n")),
                    inline: false
                  }))
                }
              ]
            }')

          # Send  the formatted JSON payload to the Discord webhook URL.
          curl -H "Content-Type: application/json" \
            -d "$DISCORD_PAYLOAD" \
            $WEBHOOK_DISCORD_CLOUDFLARE_PROJECTS

Breakdown of the workflow list_projects.yml


Workflow Triggers

  • Manual Trigger: You can run the workflow manually via the GitHub Actions UI (workflow_dispatch).
  • Scheduled Runs: The workflow crons are scheduled to run twice daily, Monday through Friday:
    • 8:30 AM UTC
    • 2:00 PM UTC

Job: sendtodos

Environment

  • Runner: ubuntu-latest
  • Permissions: Read access to repository contents, issues, and projects.

Steps

  1. Checkout Repository

    • Uses actions/checkout@v4 to access project files.
  2. Get TODOs from GitHub Projects

    • Uses actions/github-script@v7 to:
      • Query all organization-level GitHub Projects (v2)
      • Find "Status" fields and "Todo" options
      • Retrieve items in "Todo" status
      • Format results as JSON (todo_json)
  3. Send TODO List to Discord

    • If Todos exist:
      • Formats JSON into a Discord embed using jq
      • Sends payload to Discord webhook via curl
      • Embed includes project names (as links) and bulleted Todo lists

Required Secrets

  • GH_TOKEN: GitHub token for Projects/Issues access
  • WEBHOOK_DISCORD_CLOUDFLARE_PROJECTS: Discord webhook URL

Example Discord Output

Current TODOs

Here is a list of all items in the To Do column across your repository projects:

  • [Project Name](project-url)
    • • Task 1
    • • Task 2

Notes

  • Draft Issues vs. Issues: Titles are pulled from either the issue or draft issue object, with a fallback to "Draft" if missing.
  • Secret Naming: Do not prefix secrets with GITHUB_ (see GitHub Docs).
  • Adding Secrets: Go to your repo’s Settings → Secrets and variables → Actions → New repository secret.

Contact Us

We are waiting to hear from you

Find us here
Unit 12 NeedSpace
Brighton Road, Horsham
RH13 5BB
Say hello

tellmemore@azydeco.com