diff --git a/.github/workflows/newsfile.yml b/.github/workflows/newsfile.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7196f7cb6a1ec7e2bbbe01e0f28593784cb28b06
--- /dev/null
+++ b/.github/workflows/newsfile.yml
@@ -0,0 +1,22 @@
+name: Newsfile
+
+on:
+  push:
+    branches: ["develop", "release-*"]
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  changelog:
+    if: ${{ github.base_ref == 'develop'  || contains(github.base_ref, 'release-') }}
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          ref: ${{ github.event.pull_request.head.sha }}
+          fetch-depth: 0
+      - uses: actions/setup-python@v2
+      - run: pip install towncrier==21.9.0
+      - run: scripts/check-newsfragment
+        env:
+          PULL_REQUEST_NUMBER: ${{ github.event.number }}
diff --git a/changelog.d/787.misc b/changelog.d/787.misc
new file mode 100644
index 0000000000000000000000000000000000000000..eebeeb21a83a9d00f7c47351dee41dc323bd717f
--- /dev/null
+++ b/changelog.d/787.misc
@@ -0,0 +1 @@
+Add automatic changelog generation via [Towncrier](https://github.com/twisted/towncrier).
diff --git a/changelog.d/git.keep b/changelog.d/git.keep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..26bed3640962df719fed66a6c0000c206e738328
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,30 @@
+[tool.towncrier]
+    # The name of your Python package
+    filename = "CHANGELOG.md"
+    directory = "changelog.d"
+    issue_format = "[\\#{issue}](https://github.com/matrix-org/matrix-appservice-discord/issues/{issue})"
+
+    [[tool.towncrier.type]]
+        directory = "feature"
+        name = "Features"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "bugfix"
+        name = "Bugfixes"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "doc"
+        name = "Improved Documentation"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "removal"
+        name = "Deprecations and Removals"
+        showcontent = true
+
+    [[tool.towncrier.type]]
+        directory = "misc"
+        name = "Internal Changes"
+        showcontent = true
diff --git a/scripts/changelog.sh b/scripts/changelog.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2df3a58bed1cbb6193b0f0687f26d8094814217f
--- /dev/null
+++ b/scripts/changelog.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+VERSION=`python3 -c "import json; f = open('./package.json', 'r'); v = json.loads(f.read())['version']; f.close(); print(v)"`
+towncrier build --version $VERSION $1
diff --git a/scripts/check-newsfragment b/scripts/check-newsfragment
new file mode 100755
index 0000000000000000000000000000000000000000..68ff4a464e6cd1c4e6ca3127b25ee01943ace0eb
--- /dev/null
+++ b/scripts/check-newsfragment
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+#
+# A script which checks that an appropriate news file has been added on this
+# branch.
+
+
+echo -e "+++ \033[32mChecking newsfragment\033[m"
+
+set -e
+
+# make sure that origin/develop is up to date
+git remote set-branches --add origin develop
+git fetch -q origin develop
+
+pr="$PULL_REQUEST_NUMBER"
+
+# Print a link to the contributing guide if the user makes a mistake
+CONTRIBUTING_GUIDE_TEXT="!! Please see the contributing guide for help writing your changelog entry:
+https://github.com/matrix-org/matrix-appservice-bridge/blob/develop/CONTRIBUTING.md#%EF%B8%8F-pull-requests"
+
+# If check-newsfragment returns a non-zero exit code, print the contributing guide and exit
+python3 -m towncrier.check --compare-with=origin/develop || (echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2 && exit 1)
+
+echo
+echo "--------------------------"
+echo
+
+matched=0
+for f in $(git diff --diff-filter=d --name-only FETCH_HEAD... -- changelog.d | grep -xv changelog.d/git.keep); do
+    # check that any added newsfiles on this branch end with a full stop.
+    lastchar=$(tr -d '\n' < "$f" | tail -c 1)
+    if [ "$lastchar" != '.' ] && [ "$lastchar" != '!' ]; then
+        echo -e "\e[31mERROR: newsfragment $f does not end with a '.' or '!'\e[39m" >&2
+        echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2
+        exit 1
+    fi
+
+    # see if this newsfile corresponds to the right PR
+    [[ -n "$pr" && "$f" == changelog.d/"$pr".* ]] && matched=1
+done
+
+if [[ -n "$pr" && "$matched" -eq 0 ]]; then
+    echo -e "\e[31mERROR: Did not find a news fragment with the right number: expected changelog.d/$pr.*.\e[39m" >&2
+    echo -e "$CONTRIBUTING_GUIDE_TEXT" >&2
+    exit 1
+fi