Onboarding New Engineers with Self-Service Workflows

This how-to applies the scaffolder to people rather than services: a self-service workflow that provisions a new engineer’s group memberships, repository access, and starter tasks on day one, with every grant attributed and auditable. It is part of Service Onboarding Automation within Developer Experience & Self-Service Platforms, and the access grants must conform to your Team Permission Models.

Prerequisites

  • @backstage/plugin-scaffolder-backend@^1.22.0 with a custom action module for identity-provider grants.
  • ${IDP_API_TOKEN} for the identity provider (group membership) and ${GITHUB_TOKEN} for repository team membership, injected via app-config.yaml.
  • Catalog Group and User entities synced from the identity provider so the workflow can resolve teams.
  • A manager or buddy User to receive the asynchronous approval notification.
  • @backstage/cli@^0.27.0 for validation and dry-run.

Exact Configuration

1. Capture the new-hire intake

# templates/onboard-engineer.yaml
# Requires scaffolder.backstage.io/v1beta3 (Backstage >= 1.22.0)
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: onboard-engineer
  title: Onboard a New Engineer
spec:
  owner: group:people-ops
  type: onboarding
  parameters:
    - title: New engineer
      required: [userRef, team, startDate]
      properties:
        userRef:
          title: Engineer
          type: string
          ui:field: EntityPicker
          ui:options: { catalogFilter: { kind: User } }
        team:
          title: Joining team
          type: string
          ui:field: OwnerPicker
          ui:options: { catalogFilter: { kind: Group } }
        startDate: { title: Start date, type: string, format: date }

2. Grant group membership through a custom action

Repository and service-creation actions ship with Backstage; identity-provider grants are organization-specific, so register a custom action (see the backend-plugin patterns in Building Custom Backstage Plugins).

// src/scaffolder/actions/grantGroupMembership.ts
// Requires @backstage/plugin-scaffolder-node >= 0.4.0
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';

export const grantGroupMembership = createTemplateAction<{
  userRef: string;
  team: string;
}>({
  id: 'idp:group:grant',
  schema: {
    input: {
      type: 'object',
      required: ['userRef', 'team'],
      properties: {
        userRef: { type: 'string' },
        team: { type: 'string' },
      },
    },
  },
  async handler(ctx) {
    const { userRef, team } = ctx.input;
    // Token injected at runtime; never hardcode
    const res = await fetch(`${process.env.IDP_API_URL}/groups/${team}/members`, {
      method: 'POST',
      headers: { authorization: `Bearer ${process.env.IDP_API_TOKEN}` },
      body: JSON.stringify({ user: userRef }),
    });
    if (!res.ok) throw new Error(`grant failed: ${res.status}`);
    ctx.logger.info(`granted ${userRef} membership in ${team}`);
  },
});

3. Sequence the workflow with asynchronous approval

Keep human review out of the critical path: provision the safe grants immediately, and emit an approval request for anything that needs sign-off rather than blocking the run.

  steps:
    - id: grant-team
      name: Add to team group
      action: idp:group:grant
      input:
        userRef: ${{ parameters.userRef }}
        team: ${{ parameters.team }}

    - id: starter-repo-access
      name: Grant starter repository access
      action: github:repo:collaborator:add
      input:
        repoUrl: github.com?owner=${GITHUB_ORG}&repo=onboarding-tasks
        username: ${{ parameters.userRef }}
        permission: push

    - id: notify-buddy
      name: Notify onboarding buddy
      action: http:backstage:request
      input:
        method: POST
        path: /api/notifications/onboarding
        body:
          userRef: ${{ parameters.userRef }}
          startDate: ${{ parameters.startDate }}

4. Register the custom action

// src/scaffolder/index.ts
// Requires @backstage/plugin-scaffolder-backend >= 1.22.0
import { grantGroupMembership } from './actions/grantGroupMembership';
export const additionalActions = [grantGroupMembership()];

Validation

# Requires @backstage/cli >= 0.27.0
set -euo pipefail

# 1. Validate the template
npx @backstage/cli catalog validate --path ./templates/onboard-engineer.yaml
# expected: "Validated 1 entity ... 0 errors"

# 2. Unit-test the custom action against a mocked IdP
npm run test -- grantGroupMembership
# expected: "PASS  ... grants membership"

# 3. Dry-run resolves the user and team to entity refs
yq '.spec.steps[0].input.userRef' ./dry-run-output/run.yaml | grep -qE '^user:'
# expected: exit 0

Edge Cases & Troubleshooting

Symptom Root Cause Resolution
Grant succeeds but membership absent Identity-provider sync lags the grant Trigger a catalog refresh after the grant, or poll the IdP before marking the step done
Workflow blocks on approval Human gate placed in the critical path Move approval to the asynchronous notify step; provision safe grants immediately
Re-running duplicates memberships Action not idempotent Treat 409 Conflict from the IdP as success in the action handler
EntityPicker shows no users User entities not synced from the IdP Verify the org-data ingestion is running and User kind is allowed
Audit log missing the actor Action ran under a shared token Forward the requesting user’s identity into the action and log it on every grant

Frequently Asked Questions

Should engineer onboarding live in the same portal as service onboarding?

Yes — sharing one scaffolder gives both flows a single audit trail, a consistent RBAC model, and one place developers and people-ops look. The templates differ (people produce access grants, services produce repositories) but the execution surface, permission checks, and observability are identical, which is exactly the consistency you want for anything that grants access.

How do I keep access grants least-privilege during onboarding?

Grant only the memberships the joining team’s role requires and let downstream RBAC derive everything else from group membership, rather than enumerating per-repository permissions in the workflow. This keeps the onboarding template stable as projects change and concentrates authorization logic in your permission policy where it belongs.