# Background jobs

Use `crowdinApp.cron.schedule(expression, task)` to schedule background work. For work across installed accounts, `forEachCrowdinConnection` supplies an authenticated Crowdin API client for each stored installation, renews tokens automatically, and isolates installation failures.

## Schedule a job

```ts
import { addCrowdinEndpoints, express } from '@crowdin/app-project-module';

const app = express();
const crowdinApp = addCrowdinEndpoints(app, {
  name: 'Project cache',
  identifier: 'project-cache',
  description: 'Refresh project metadata hourly',
  clientId: process.env.CLIENT_ID!,
  clientSecret: process.env.CLIENT_SECRET!,
});

crowdinApp.cron.schedule('0 * * * *', async () => {
  const result = await crowdinApp.forEachCrowdinConnection(async ({ client, credentials }) => {
    const projects = await client.projectsGroupsApi.withFetchAll().listProjects();
    await crowdinApp.saveMetadata({
      id: 'project-cache',
      crowdinId: credentials.id,
      metadata: JSON.stringify(projects.data.map(({ data }) => data)),
    });
  });
  console.log(`Updated ${result.processed}/${result.total} installations; ${result.failed.length} failed`);
});

app.listen(3000);
```

The scheduler defaults to Node.js cron. For serverless hosting, provide a custom `Cron` implementation and invoke it from the hosting platform's scheduled handler; see [Cloudflare Workers deployment](/app-project-module/deployment/#cloudflare-workers). The helper itself does not schedule work and can also be awaited from an admin route or webhook handler.

**Tip:** Project integrations should use [`projectIntegration.cronJobs`](/app-project-module/project-integration/synchronization/) for work requiring integration credentials, project settings, and root folders.

## Options and result

| Option | Default | Behavior |
| --- | --- | --- |
| `concurrency` | `CRON_ORG_CONCURRENCY`, or `5` | Positive safe integer; installations run in batches of at most this size. |
| `checkSubscription` | `true` | Skip installations whose subscription is expired. Set to `false` for work that must also run for expired subscriptions, such as cleanup. |

The callback receives `{ client, credentials }`. `credentials` is the original storage snapshot identifying the installation, not a source of current API tokens. Use `client` for API calls. The callback runs once per stored installation; enumerate projects inside it when needed.

The result contains `total`, `processed`, `skippedExpired`, and `failed` (`{ crowdinId, error }[]`). `processed` counts successfully completed callbacks; each installation has exactly one outcome. Client preparation and callback errors are logged and collected without aborting other installations. Empty storage returns zero counts and an empty `failed` array.

Invalid explicit concurrency rejects with `RangeError`; a storage listing failure rejects the whole run. Unauthorized configuration throws immediately. Subscription checks retain the SDK's existing behavior: handled subscription-service HTTP errors allow processing to continue.

## Authentication and execution limits

Background token preparation uses the existing `crowdin_app`, `authorization_code`, and `crowdin_agent` flows. `crowdin_app_with_code` requires a code from a request JWT and is not supported for background iteration: each stored installation is reported in `failed`, and its callback is not called.

Concurrency applies to one invocation only. Separate invocations or app replicas can overlap; this helper adds no retries or distributed locking. Design callbacks to tolerate repeated execution where needed. Avoid logging whole credentials or result errors without the application's normal error sanitization.