Recipe execution and commits with the Moderne API
Imagine you found a recipe you would like to run as part of your organization's automation process (such as updating the Gradle plugin version when a new release is published). Rather than manually running this recipe each time, you can use Moderne's GraphQL API to speed this process up with automation.
To help you understand how to automate recipe execution and commits, we'll walk through all the steps necessary to use Moderne's GraphQL API. By the end, you should know how to:
- Execute recipes
- Verify that recipes have been completed
- Retrieve repository results
- Download data tables
- Commit changes
- Ensure that committed changes are correct
Prerequisites
This guide assumes that you:
- Know how to use and interact with GraphQL APIs.
- Have created a Moderne personal access token.
- Have created an SCM access token.
Recipe execution
-
To begin, you'll want to decide what repositories you want your recipe to run on. You have three options for selecting repositories: choosing an existing organization, creating a new user-defined organization, or selecting an existing user-defined organization. Once you've selected or created one, you can proceed to step 2.
-
Navigate to the recipe you wish to run and fill out the recipe options.
-
At the bottom of the recipe, you will find an API examples button. Click on it and then select Run a recipe. This will provide you with the query that will be run when executing a recipe run. Additionally, the appropriate variables will be added to this query based on your organization selection from step 1.

- You can then execute a recipe with the following mutation:
- Run Recipe Mutation
- Mutation Variables
- cURL
mutation runRecipe($input: RunRecipeInput!) {
runRecipe(input: $input) {
id
}
}
{
"input": {
"recipe": {
"id": "org.openrewrite.gradle.plugins.UpgradePluginVersion",
"options": [
{ "name": "pluginIdPattern", "value": "com.gradle.plugin-publish" },
{ "name": "newVersion", "value": "1.1.0" }
]
},
"organizationId": "Gradle"
}
}
curl --request POST \
--url https://api.app.moderne.io/graphql \
--header 'Authorization: Bearer <YOUR MODERNE TOKEN HERE>' \
--header 'Content-Type: application/json' \
--data '{"query":"mutation runRecipe($input: RunRecipeInput!) {\n runRecipe(input: $input) {\n id\n }\n}","variables":{"input":{"recipe":{"id":"org.openrewrite.gradle.plugins.UpgradePluginVersion","options":[{"name":"pluginIdPattern","value":"com.gradle.plugin-publish"},{"name":"newVersion","value":"1.1.0"}]},"organizationId":"Gradle"},"operationName":"runRecipe"}'
Each recipe option is provided as a name and value pair. The value accepts whatever type the option expects, such as a string, a boolean, a number, or a list.
- The mutation returns the
idof the recipe run. Thisididentifies the run (a changeset in the Moderne API) and is used in every subsequent step, including polling, data table downloads, and commits. Example response:
{
"data": {
"runRecipe": {
"id": "20260723153349-gueAs"
}
}
}
Verify recipe completion
In the Moderne API, a recipe run is a changeset that belongs to the organization it ran against. Rather than a separate state field, the run's current state is encoded in its __typename (for example, OrganizationRecipeRunRunning or OrganizationRecipeRunFinished). To poll a run, you will need both the organizationId you ran against and the run id from the previous step.
- Poll (Moderne's web interface uses a 3-second interval) with the query below. Every state writer advances the
lastUpdatedAthigh-water mark, so you can poll__typenameandlastUpdatedAtcheaply and only fetch the heavier results once the run reaches a terminal state.
- Recipe Run State Query
- Query Variables
query recipeRunState($orgId: ID!, $runId: ID!) {
organization(id: $orgId) {
changesets(where: { id: { _eq: $runId } }) {
edges {
node {
__typename
id
... on OrganizationRecipeRun {
lastUpdatedAt
recipe {
id
displayName
}
}
}
}
}
}
}
{
"orgId": "Gradle",
"runId": "20260723153349-gueAs"
}
- Keep polling until the run's
__typenamereaches a terminal state:OrganizationRecipeRunFinished,OrganizationRecipeRunError, orOrganizationRecipeRunCanceled. A run that is still in progress reportsOrganizationRecipeRunQueuedorOrganizationRecipeRunRunning. Example response:
{
"data": {
"organization": {
"changesets": {
"edges": [
{
"node": {
"__typename": "OrganizationRecipeRunFinished",
"id": "20260723153349-gueAs",
"lastUpdatedAt": "2026-07-23T15:34:40.812Z",
"recipe": {
"id": "org.openrewrite.gradle.plugins.UpgradePluginVersion",
"displayName": "Update a Gradle plugin by id"
}
}
}
]
}
}
}
}
Retrieve repositories with results
Once the run has finished, you can retrieve the repositories where changes were made from the same changeset. On a finished run, the repositories connection accepts a where filter, and passing onlyWithResults: true limits the results to repositories that actually changed. Each repository also carries its own __typename reflecting its per-repository outcome (for example, RepositoryRecipeRunFinished, RepositoryRecipeRunError, or RepositoryRecipeRunNoLst).
- Using the
organizationIdand the runid, retrieve the changed repositories with the query below.
- Recipe Run Results Query
- Query Variables
query recipeRunResults($orgId: ID!, $runId: ID!, $first: Int, $after: String) {
organization(id: $orgId) {
changesets(where: { id: { _eq: $runId } }) {
edges {
node {
__typename
... on OrganizationRecipeRunFinished {
totals {
filesChanged
repositoriesWithResults
repositoriesSuccessful
repositoriesWithErrors
}
repositories(
first: $first
after: $after
where: { onlyWithResults: true }
) {
count
pageInfo {
hasNextPage
endCursor
}
edges {
node {
__typename
repository {
origin
path
branch
}
}
}
}
}
}
}
}
}
}
{
"orgId": "Gradle",
"runId": "20260723153349-gueAs",
"first": 100
}
- The
repositoriesconnection is paginated. WhenpageInfo.hasNextPageistrue, passpageInfo.endCursorback as theaftervariable to fetch the next page. Use each repository'sorigin,path, andbranchto build the repository list for the commit step. Example response:
{
"data": {
"organization": {
"changesets": {
"edges": [
{
"node": {
"__typename": "OrganizationRecipeRunFinished",
"totals": {
"filesChanged": 3,
"repositoriesWithResults": 2,
"repositoriesSuccessful": 2,
"repositoriesWithErrors": 0
},
"repositories": {
"count": 2,
"pageInfo": {
"hasNextPage": false,
"endCursor": "MQ=="
},
"edges": [
{
"node": {
"__typename": "RepositoryRecipeRunFinished",
"repository": {
"origin": "github.com",
"path": "gradle/gradle-checksum",
"branch": "master"
}
}
},
{
"node": {
"__typename": "RepositoryRecipeRunFinished",
"repository": {
"origin": "github.com",
"path": "gradle-nexus/publish-plugin",
"branch": "master"
}
}
}
]
}
}
}
]
}
}
}
}
Downloading data tables
Data tables are exported asynchronously. You start a download with the downloadDataTable mutation and then poll the run's dataTables connection until the download is ready. Like recipe runs, a data table's state is encoded in its __typename.
- Start the download by specifying the changeset (your recipe run
id), the data table to export, and the format (CSVorXLSX). The following mutation requests theorg.openrewrite.table.SourcesFileResultsdata table:
- Start Data Table Download Mutation
- Mutation Variables
mutation startDataTableDownload($changesetId: ID!, $dataTable: String!, $format: DataTableFormat!) {
downloadDataTable(
changesetId: $changesetId
dataTable: $dataTable
format: $format
) {
__typename
id
dataTable {
name
displayName
}
}
}
{
"changesetId": "20260723153349-gueAs",
"dataTable": "org.openrewrite.table.SourcesFileResults",
"format": "CSV"
}
The mutation returns the download task in its DataTableProcessing state, along with the task id you will poll:
{
"data": {
"downloadDataTable": {
"__typename": "DataTableProcessing",
"id": "20260723155616-Om91d",
"dataTable": {
"name": "org.openrewrite.table.SourcesFileResults",
"displayName": "Source files that had results"
}
}
}
}
For community data tables that belong to a group, pass the optional group argument to select the correct bucket.
- Because the export takes time, poll the run's
dataTablesconnection with the taskiduntil the download finishes:
- Data Table Download Query
- Query Variables
query dataTableDownloadState($orgId: ID!, $runId: ID!, $dataTableId: ID!) {
organization(id: $orgId) {
changesets(where: { id: { _eq: $runId } }) {
edges {
node {
dataTables(where: { id: { _eq: $dataTableId } }) {
edges {
node {
__typename
id
... on DataTableFinished {
format
downloadUrl
}
... on DataTableError {
message
}
}
}
}
}
}
}
}
}
{
"orgId": "Gradle",
"runId": "20260723153349-gueAs",
"dataTableId": "20260723155616-Om91d"
}
The data table moves through states you can read from its __typename: DataTableAvailable, DataTableProcessing, DataTableFinished, and DataTableError. Keep polling until the node reports DataTableFinished, which exposes the downloadUrl:
{
"data": {
"organization": {
"changesets": {
"edges": [
{
"node": {
"dataTables": {
"edges": [
{
"node": {
"__typename": "DataTableFinished",
"id": "20260723155616-Om91d",
"format": "CSV",
"downloadUrl": "/api/recipe-runs/20260723153349-gueAs/20260723153353-ftHOe/datatable/org.openrewrite.table.SourcesFileResults.csv.gz"
}
}
]
}
}
}
]
}
}
}
}
- The
downloadUrlis a path relative to the API base URL, so the full download location ishttps://api.app.moderne.iofollowed by thedownloadUrlvalue. Use the returned value as-is rather than building it by hand, since it embeds internal identifiers. Note that the exported file is gzipped (for example, aCSVexport is served as.csv.gz).
Creating a pull request
The Moderne API exposes a single commit mutation for delivering a changeset's results back to your repositories. You choose how the changes are delivered through the strategy field, which accepts exactly one of direct (push to the origin remote), fork, pullRequest, or forkAndPullRequest. To open pull requests, use the pullRequest strategy.
The scmAccessTokens field inside input is required for programmatic pull request creation. If you don't provide this token, the API will initiate an OAuth browser flow, which is not suitable for automation or scripting. Make sure you've created an SCM access token and include it in your request as shown in the mutation variables example.
- Perform the
commitmutation using the runidas thechangesetIdand the repositories from the previous step. Therepositoriesfield is a list of filters: each entry matches repositories in the changeset (for example, bypath). Omitrepositoriesentirely to commit to every repository with results in the run.
- Commit Mutation
- Mutation Variables
- cURL
mutation commit($input: CommitInput!) {
commit(input: $input) {
id
}
}
{
"input": {
"organizationId": "Gradle",
"changesetId": "20260723153349-gueAs",
"message": "refactor: Update a Gradle plugin by id",
"repositories": [
{ "path": { "_eq": "gradle/gradle-checksum" } },
{ "path": { "_eq": "gradle-nexus/publish-plugin" } }
],
"strategy": {
"pullRequest": {
"title": "refactor: Update a Gradle plugin by id",
"body": "cmVmYWN0b3I6IFVwZGF0ZSBhIEdyYWRsZSBwbHVnaW4gYnkgaWQ=",
"draft": false
}
},
"scmAccessTokens": [
{ "origin": "github.com", "value": "MY_SCM_PERSONAL_ACCESS_TOKEN" }
]
}
}
curl --request POST \
--url https://api.app.moderne.io/graphql \
--header 'Authorization: Bearer <YOUR MODERNE TOKEN HERE>' \
--header 'Content-Type: application/json' \
--data '{"query":"mutation commit($input: CommitInput!) {\n commit(input: $input) {\n id\n }\n}","variables":{"input":{"organizationId":"Gradle","changesetId":"20260723153349-gueAs","message":"refactor: Update a Gradle plugin by id","repositories":[{"path":{"_eq":"gradle/gradle-checksum"}},{"path":{"_eq":"gradle-nexus/publish-plugin"}}],"strategy":{"pullRequest":{"title":"refactor: Update a Gradle plugin by id","body":"cmVmYWN0b3I6IFVwZGF0ZSBhIEdyYWRsZSBwbHVnaW4gYnkgaWQ=","draft":false}},"scmAccessTokens":[{"origin":"github.com","value":"MY_SCM_PERSONAL_ACCESS_TOKEN"}]},"operationName":"commit"}'
The body is Base64-encoded. If you omit title, the commit message is used as the pull request title.
- The mutation returns the commit
id, which you use to poll the commit's progress. Example response:
{
"data": {
"commit": {
"id": "c83315a1-397f-44cb-9ef2-9a2ca195dda6"
}
}
}
Verify commit job
A commit runs asynchronously across the repositories you targeted. A commit belongs to the changeset it was created from, so you poll it through the run's commits connection, filtered by the commit id, and read its __typename for the overall state. The repositories connection reports per-repository progress: compare completedCount to count to track how many repositories have reached a terminal state.
- Poll for the commit's completion with the query below. Each repository's
__typenamereflects its individual outcome, such asPullRequestCommitSucceeded(which exposes aresultLinkto the pull request and itspullRequestStatus),RepositoryCommitFailed, orRepositoryCommitNoChanges.
- Commit State Query
- Query Variables
query commitState($orgId: ID!, $runId: ID!, $commitId: ID!, $first: Int, $after: String) {
organization(id: $orgId) {
changesets(where: { id: { _eq: $runId } }) {
edges {
node {
commits(where: { id: { _eq: $commitId } }) {
edges {
node {
__typename
id
message
repositories(first: $first, after: $after) {
count
completedCount
pageInfo {
hasNextPage
endCursor
}
edges {
node {
__typename
repository {
origin
path
branch
}
... on PullRequestCommitSucceeded {
resultLink
pullRequestStatus {
state
}
}
... on RepositoryCommitFailed {
errorMessage
}
}
}
}
}
}
}
}
}
}
}
}
{
"orgId": "Gradle",
"runId": "20260723153349-gueAs",
"commitId": "c83315a1-397f-44cb-9ef2-9a2ca195dda6",
"first": 50
}
- Keep polling until the commit's
__typenamereaches a terminal state:OrganizationCommitFinished,OrganizationCommitCanceled, orOrganizationCommitError. A commit still in progress reportsOrganizationCommitQueuedorOrganizationCommitRunning. Example response:
{
"data": {
"organization": {
"changesets": {
"edges": [
{
"node": {
"commits": {
"edges": [
{
"node": {
"__typename": "OrganizationCommitFinished",
"id": "c83315a1-397f-44cb-9ef2-9a2ca195dda6",
"message": "refactor: Update a Gradle plugin by id",
"repositories": {
"count": 2,
"completedCount": 2,
"pageInfo": {
"hasNextPage": false,
"endCursor": "MQ=="
},
"edges": [
{
"node": {
"__typename": "PullRequestCommitSucceeded",
"repository": {
"origin": "github.com",
"path": "gradle/gradle-checksum",
"branch": "master"
},
"resultLink": "https://github.com/gradle/gradle-checksum/pull/14",
"pullRequestStatus": {
"state": "OPEN"
}
}
},
{
"node": {
"__typename": "PullRequestCommitSucceeded",
"repository": {
"origin": "github.com",
"path": "gradle-nexus/publish-plugin",
"branch": "master"
},
"resultLink": "https://github.com/gradle-nexus/publish-plugin/pull/8",
"pullRequestStatus": {
"state": "OPEN"
}
}
}
]
}
}
}
]
}
}
}
]
}
}
}
}