A runnable GitHub App that writes external custom properties to repositories in an organization.
New to external custom properties? Start with Integrating custom properties with an external system, which explains what they are, how namespacing and registration work, and the permissions an app needs. This README covers configuring and running this server.
- Prerequisites
- Permissions and who registers
- Step 1: Get a Webhook Proxy URL
- Step 2: Register a GitHub App
- Step 3: Generate and Store Credentials
- Step 4: Run the Example Server
- Step 5: Install the App on Your Organization
- Step 6: Test the Integration
- API Reference
- Reading external custom property values
- Troubleshooting
- Contributing
- Support
- Maintainers
- Security
- License
- Node.js version 24 or greater, which includes a compatible npm (download)
- A GitHub organization with at least one repository
- A webhook proxy for local development — this guide uses Smee.io
If you're new to building GitHub Apps, the Quickstart for building GitHub Apps provides a general introduction to the concepts used here.
Before an app can write values, its installation must be registered with a display name. The app can do this itself, or an organization administrator can do it on the app's behalf — which one applies depends on the permission level the app is granted.
This example server registers itself, so it asks for Admin on the "External custom properties for repositories" organization permission.
For the full permission model — including when to choose Admin versus Read and write, and who can register on an app's behalf — see Selecting permissions. The access level and token types each endpoint requires are listed in Permissions required for GitHub Apps.
In order to develop your app locally, you need a way to forward webhooks from GitHub to your local machine. This guide uses Smee.io as a free webhook proxy.
- In your browser, navigate to https://smee.io/
- Click Start a new channel
- Copy the full URL under "Webhook Proxy URL" (e.g.
https://smee.io/abc123...)
Save this URL — you'll use it when registering your GitHub App and when starting the example server.
Note: Smee.io is intended for local development only. When you deploy your app to production, you'll replace this with your server's public webhook endpoint.
For full details on app registration, see Registering a GitHub App. The steps below cover the settings specific to External custom properties.
-
Navigate to your organization's settings: Organization page → Settings → Developer settings → GitHub Apps → New GitHub App
-
Fill in the basic details:
- GitHub App name: e.g.
my-org-external-custom-properties - Homepage URL: Your app's repository or company URL
- GitHub App name: e.g.
-
Configure webhooks:
- Webhooks: Ensure "Active" is checked
- Webhook URL: Your Smee.io proxy URL from Step 1
- Webhook secret: Enter a random secret string — save this for later
-
Set permissions:
- Under Organization permissions, find "External custom properties for repositories" and select Admin.
Why Admin? This example server registers its own namespace on installation, which requires Admin. If you'd rather an org owner control namespacing, select Read and write instead — see Selecting permissions.
-
Subscribe to events:
- Check the Installation event (this fires when the app is installed on an org)
Note: The example server uses the
installation.createdevent to register and write an initial batch of properties as soon as the app is installed. If you're building your own implementation, consider which events make sense for your use case — for example,repository.createdto tag new repos,pushto update properties after deploys, or no webhook at all if you prefer a purely scheduled/cron-based approach. -
Installation scope:
- Select "Only on this account" (you can change this later)
-
Click Create GitHub App
-
After creating the app, note the App ID shown on the app settings page.
-
Under "Private keys", click Generate a private key. A
.pemfile will download — keep this safe. -
In the
example-server/directory, copy the env template and fill in your values:cd example-server cp .env.example .env -
Open
.envand update the values:APP_ID="12345" WEBHOOK_SECRET="your-webhook-secret" PRIVATE_KEY_PATH="./your-app-name.2026-06-10.private-key.pem" DISPLAY_NAME="acme"- APP_ID: The App ID from your app's settings page
- WEBHOOK_SECRET: The webhook secret you chose in Step 2
- PRIVATE_KEY_PATH: Path to the
.pemfile you downloaded - DISPLAY_NAME: The namespace display name to register. Properties appear in GitHub as
<DISPLAY_NAME>.<property_name>. For the length and character rules, see Register an app installation for external custom properties.
-
Move the downloaded
.pemfile into theexample-server/directory (or update the path in.envto point to its location).
The example-server/ directory contains a Node.js application that demonstrates the typical calling pattern:
- Listens for the
installation.createdwebhook — when the app is installed on an org, it:- Registers the namespace (
DISPLAY_NAME) for the installation. - Writes a set of external custom properties to the org's first repository.
- Reads the property definitions back to verify the write.
- Registers the namespace (
- Runs a periodic sync — every 60 minutes by default, it iterates all installations, fetches the first repo in each org, and re-writes properties (including a
last_syncedtimestamp so you can verify the sync is working). Configurable throughSYNC_INTERVAL_MINUTES, clamped to between 5 minutes and one week. The sync assumes the installation is already registered.
Every API call lives in a clearly-named helper function — registerNamespace, writeExternalCustomProperties, and readOrgSchema for the automatic flow, plus getRegisteredInstallations, updateExternalCustomPropertyValues, deleteExternalCustomPropertyValues, readRepositoryCustomPropertyValues, readRepositoryCustomPropertiesWithGraphql, and readRepositoryCustomPropertyWithGraphql — so you can copy them into your own implementation.
The automatic webhook flow is register → batch write → definitions read. The remaining helpers list visible registrations, sparsely update or unset one property, explicitly delete one property, and read values with REST or GraphQL. They are not wired into the webhook, periodic sync, or startup flow. app.js collects them in runOptInExamples, which nothing calls: run it yourself to see the read-only calls against the first repository in each organization. The two examples that change values stay commented out, so the destructive delete is never called automatically.
For a detailed walkthrough of how GitHub Apps handle webhook events in Node.js, see Building a GitHub App that responds to webhook events.
cd example-server
npm installnpx smee -u YOUR_WEBHOOK_PROXY_URL -t http://localhost:3000/api/webhookReplace YOUR_WEBHOOK_PROXY_URL with the URL from Smee.io.
npm run serverYou should see:
Server is listening for events at: http://localhost:3000/api/webhook
Registering namespace "acme" on new installations
Periodic sync scheduled every 60 minutes
- From your app's settings page, click "Public page" (in the left sidebar)
- Click Install
- Select your organization
- Click Install
Note: You won't be asked to pick individual repositories. See Install the app for why.
When the installation completes, GitHub sends an installation.created webhook event to your configured webhook URL. The example server registers the namespace, writes external custom properties to the org's first repository, and reads the schema back.
- Install the app on your organization (Step 5 above)
- Watch the terminal running the server — you should see:
Received installation.created event for org: my-org Registering namespace "acme" for org: my-org Registered namespace "acme" for installation 42 in my-org Fetching repositories for org: my-org... Writing external custom properties to repos in my-org: my-repo Successfully wrote external custom properties in my-org (HTTP 204) Reading external custom property schema for org: my-org Registered property names for my-org: environment, last_synced, service, team - Navigate to your organization's Settings → Custom properties to see the external custom properties attached to your repository (shown as
acme.environment,acme.service, etc.) - Wait for the periodic sync (or set
SYNC_INTERVAL_MINUTES=5in.env, the fastest the sync will run, to test more quickly) and verify that thelast_syncedproperty updates with the current timestamp
Each endpoint links to its entry in the REST API reference, which documents the request body, responses, and error cases for that endpoint. Between them, those entries also cover display name rules, property name and value constraints, request size limits, and cleanup behavior. The access level and token types each endpoint requires are listed in Permissions required for GitHub Apps. The setup guide lists them in the order an integration typically calls them, and each has a matching helper in example-server/app.js.
| Endpoint | Helper function |
|---|---|
POST /orgs/{org}/properties/installations |
registerNamespace |
GET /orgs/{org}/properties/installations |
getRegisteredInstallations |
GET /orgs/{org}/properties/installations/schema |
readOrgSchema |
PATCH /orgs/{org}/properties/installations/values |
writeExternalCustomProperties |
PATCH /orgs/{org}/properties/installations/values/{property_name} |
updateExternalCustomPropertyValues |
DELETE /orgs/{org}/properties/installations/values/{property_name} |
deleteExternalCustomPropertyValues |
External custom properties are read back through the existing custom properties APIs — there is no separate read API. Use the namespace-qualified name, such as acme.environment.
- REST: Get all custom property values for a repository
- GraphQL: the
repositoryCustomPropertyValuesandrepositoryCustomPropertyValuefields on theRepositoryobject
For working examples of both, see readRepositoryCustomPropertyValues, readRepositoryCustomPropertiesWithGraphql, and readRepositoryCustomPropertyWithGraphql in example-server/app.js.
Error responses and their meanings are documented with each endpoint in the REST API reference.
- Verify your Smee proxy is running and connected
- Check that the app's webhook URL matches your Smee channel URL
- Ensure the "Installation" event is subscribed in your app settings
- Check the Advanced tab on your app's settings page to see recent webhook deliveries and any failures
Contributions are welcome. See CONTRIBUTING.md for how to set up the project and submit a pull request, and CODE_OF_CONDUCT.md for the terms of participation.
This sample is under active development and maintained by GitHub staff. See SUPPORT.md for how to get help, including where to raise questions about the API itself rather than this sample.
Maintained by the owners listed in CODEOWNERS.
See SECURITY.md for how to report a security vulnerability. Please do not report vulnerabilities through public issues or pull requests.
This project is licensed under the terms of the MIT open source license. See LICENSE for the full terms.