Recipe
The nightly agent
A first-party GitHub Actions workflow that wakes up at 02:17 every night, asks your agent to pull one item off your FeatureJet queue, and saves the resulting diff for you to read over coffee. Copy two files, set three values, and the loop runs while you sleep.
Who runs what
GitHub Actions is the scheduler; FeatureJet is the queue. FeatureJet does not run or schedule your agent, and this recipe does not change that — it is the scheduler we happen to use, written down. Any cron, launchd job, CI runner, or agent-platform scheduler works the same way: something on your side triggers the pull, and the board answers the question of what to build next.
1. Copy the workflow
The workflow belongs in the repository the agent should change — not in a FeatureJet checkout. Save it as .github/workflows/featurejet-nightly-agent.yml in that repository.
name: FeatureJet nightly agent
on:
schedule:
- cron: "17 2 * * *"
workflow_dispatch:
inputs:
agent_command:
description: "Optional one-run override for the command that reads the prompt on stdin"
required: false
type: string
permissions:
contents: read
jobs:
nightly-agent:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Check out the caller repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Pull one planned request and run the configured agent
env:
FEATUREJET_API_KEY: ${{ secrets.FEATUREJET_API_KEY }}
FEATUREJET_BOARD_SLUG: ${{ vars.FEATUREJET_BOARD_SLUG }}
AGENT_COMMAND_OVERRIDE: ${{ inputs.agent_command }}
AGENT_COMMAND_DEFAULT: ${{ vars.FEATUREJET_AGENT_COMMAND }}
run: |
set -euo pipefail
if [[ -z "${FEATUREJET_API_KEY:-}" ]]; then
echo "Missing required Actions secret: FEATUREJET_API_KEY" >&2
exit 1
fi
if [[ -z "${FEATUREJET_BOARD_SLUG//[[:space:]]/}" ]]; then
echo "Missing required Actions variable: FEATUREJET_BOARD_SLUG" >&2
exit 1
fi
if [[ ! "$FEATUREJET_BOARD_SLUG" =~ ^[a-z0-9]+([a-z0-9-]*[a-z0-9])?$ ]]; then
echo "FEATUREJET_BOARD_SLUG must be lowercase letters, numbers, and hyphens" >&2
exit 1
fi
agent_command="$AGENT_COMMAND_OVERRIDE"
if [[ -z "${agent_command//[[:space:]]/}" ]]; then
agent_command="$AGENT_COMMAND_DEFAULT"
fi
if [[ -z "${agent_command//[[:space:]]/}" ]]; then
echo "Missing required Actions variable: FEATUREJET_AGENT_COMMAND" >&2
echo "Set it to a command that reads its prompt from stdin." >&2
exit 1
fi
mkdir -p "$RUNNER_TEMP/featurejet-agent-review"
git rev-parse HEAD > "$RUNNER_TEMP/featurejet-agent-base-sha"
prompt=$(cat <<EOF
Read and implement the highest-voted planned request on the FeatureJet board "${FEATUREJET_BOARD_SLUG}" in this checked-out repository.
Use the hosted FeatureJet MCP server at https://mcp.featurejet.com/mcp with the FEATUREJET_API_KEY already present in your environment. Follow this current queue-pull sequence exactly:
1. Call get_queue for board slug "${FEATUREJET_BOARD_SLUG}". It returns the top unclaimed posts in the board owner's curated order.
2. If a post is returned, call get_post for the first post and list_comments for that same post before deciding what to change.
3. If the queue is empty, report that no work was selected and leave the repository unchanged.
Work only in this repository. Run the relevant tests. Do not call update_post, create_comment, or any other FeatureJet write tool; do not change a FeatureJet status, push a branch, or open a pull request. The resulting working-tree diff will be saved for a human review.
EOF
)
printf '%s\n' "$prompt" | bash -lc "$agent_command"
- name: Capture the resulting diff for review
if: always()
run: |
set -euo pipefail
review_dir="$RUNNER_TEMP/featurejet-agent-review"
mkdir -p "$review_dir"
if [[ ! -s "$RUNNER_TEMP/featurejet-agent-base-sha" ]]; then
echo "The agent base revision was not recorded." > "$review_dir/capture-error.txt"
exit 1
fi
base_sha="$(<"$RUNNER_TEMP/featurejet-agent-base-sha")"
git diff --binary "$base_sha" > "$review_dir/featurejet-agent.diff"
git status --short > "$review_dir/featurejet-agent.status"
git ls-files --others --exclude-standard -z > "$review_dir/featurejet-agent.untracked"
while IFS= read -r -d '' path; do
diff_status=0
git diff --no-index --binary /dev/null -- "$path" >> "$review_dir/featurejet-agent.diff" || diff_status=$?
if (( diff_status > 1 )); then
echo "Could not include untracked path in the review diff: $path" >&2
exit "$diff_status"
fi
done < "$review_dir/featurejet-agent.untracked"
- name: Upload review artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: featurejet-nightly-agent-${{ github.run_id }}
path: ${{ runner.temp }}/featurejet-agent-review
if-no-files-found: error
retention-days: 14
2. Configure three values
In that repository's Settings → Secrets and variables → Actions. The key is a secret; the other two are plain repository variables. A missing key, slug, or command fails the run with a clear configuration error before the agent starts.
FEATUREJET_API_KEY
Secret
A FeatureJet API key. A propose-scoped key is the right shape here.
FEATUREJET_BOARD_SLUG
Variable
The lowercase, hyphenated slug of the board the agent works from.
FEATUREJET_AGENT_COMMAND
Variable
A command that reads its prompt from stdin, e.g. my-agent run --stdin.
The workflow reads the key only from secrets.FEATUREJET_API_KEY and passes it to your agent through the environment. Do not put a key in the command or in a repository variable — the variable is executed by the runner, so review it the way you review code.
3. What the prompt asks for
Pull one item
get_queue returns the top unclaimed posts in your curated order — ranked posts first, then unranked planned posts by votes. An empty queue means the run selects no work and changes nothing.
Read the request the way your users wrote it
get_post and list_comments on the winner, so the agent has the request and the thread where the edge cases usually live, before it decides what to change.
Change code, not statuses
The prompt forbids update_post, create_comment, and every other FeatureJet write tool. The agent edits and tests the checked-out repository; the status change that emails your voters stays yours.
4. Review the run
Every run uploads an artifact named featurejet-nightly-agent-<run-id> and keeps it for 14 days:
featurejet-agent.diff
A binary-capable patch from the checked-out revision, including untracked files.
featurejet-agent.status
The final short Git status.
featurejet-agent.untracked
NUL-delimited paths that were untracked at capture time.
Inspect the artifact, merge the change through your normal process, and then update the FeatureJet post yourself. That status flip is what tells the people who voted that the thing they asked for exists.
The honest part
What this recipe does not guarantee
The workflow has read-only GitHub contents permission, never pushes a branch, and never opens a pull request. But FEATUREJET_AGENT_COMMAND is an arbitrary command and it receives a real FeatureJet key. A command that ignores the prompt could still call write tools or use other credentials it finds. The human gate here depends on the command honouring the prompt — the workflow cannot enforce it.
Two things narrow that: hand the workflow a propose-scoped key, which cannot flip a status no matter what the command does; and use a command you trust.
This recipe also does not claim a post. If you point more than one agent at the same board, add a claim_post step so two runs cannot build the same request.
The tool reference and per-client setup live in the MCP docs, and the full loop — users vote, an agent pulls, voters hear back — is on the agent work queue page, including what agents can and cannot do yet.