Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Oct 9, 2026

Quick Start: Minimal SAM Integration (Mapping)

Get mapping data flowing from Data Center to your cloud app in 30 minutes.

What you'll build

A minimal Simplified App Migration (SAM) integration that:

  • Receives ID mapping data (Data Center → Cloud identifier mappings)
  • Downloads and processes mapping files
  • Acknowledges successful processing

By the end, your cloud app will receive Jira issue or Confluence page ID mappings during a migration.

Time: ~30 minutes

Difficulty: Beginner

Export type: Mappings (Preview)

Before you begin

You'll need:

How SAM works: Simplified App Migration packages data during a Data Center to Cloud migration. When you run a migration using JCMA, the platform exports your app's data (as specified in your manifest) and notifies your cloud app when it's ready.

For mappings, the data becomes available after the product migration completes (because DC-to-Cloud ID mappings only exist once entities have been migrated to cloud).

Mappings are in Preview and available for production migrations. Other SAM features (binary, AO tables) are Early Access only.


Step 1: Declare the managed export

Tell the migration platform to package mapping data for your app.

Add to your cloud app manifest

Add to your Forge manifest.yml:

1
2
3
4
5
6
7
modules:
  migration:managedExports:
    - key: mapping
      entityTypes:
        - "jira:issue"
        - "confluence:page"

Choose the entity types your app needs mappings for. Common types:

  • jira:issue - Jira issue ID mappings
  • jira:project - Jira project ID mappings
  • confluence:page - Confluence page ID mappings
  • confluence:space - Confluence space ID mappings

See the full list in Mappings reference.

Deploy your app

Deploy the updated manifest to development environment and install on your test cloud site:

1
2
3
forge deploy --environment development
forge install --site your-test-site.atlassian.net --environment development

The --environment development flag explicitly deploys to your development environment (this is also the CLI default). Always test SAM integrations in development or staging before deploying to production with --environment production.


Step 2: Subscribe to the event

Your cloud app needs to listen for the event-file-list-uploaded notification.

Add event subscription

Add to manifest.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app:
  id: [your-app-id]

triggers:
  - key: migration-event-trigger
    function: migration-handler
    events:
      - avi:ecosystem.migration:uploaded:event_file_list
      - avi:ecosystem.migration:listener_triggered
      - avi:ecosystem.migration:transfer_settled
endpoint:
  - key: remote-event
    remote: remote-app-ref
    route:
      path: /api/forge/events
    auth:
      appSystemToken:
        enabled: true

Create the event handler

Create an HTTP endpoint to receive events at /api/forge/events (as defined in the manifest):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Express.js endpoint handler
app.post('/api/forge/events', async (req, res) => {
  const event = req.body;
  const { eventType } = event;

  console.log(`Received migration event: ${eventType}`);

  try {
    if (eventType === 'event-file-list-uploaded') {
      await handleFileListUploaded(event);
    }
    // Handle other events (listener-triggered, transfer-settled, etc.)

    res.status(200).send({ success: true });
  } catch (error) {
    console.error('Event handling failed:', error);
    res.status(500).send({ error: error.message });
  }
});

async function handleFileListUploaded(event) {
  const { key, label, isComplete, transferId, movementDetails } = event;

  console.log(`File list uploaded - Label: ${label}, IsComplete: ${isComplete}`);

  if (label === 'MAPPINGS') {
    // TODO: Process mappings in next step
  }
}

Step 3: Download the file list

When you receive the event-file-list-uploaded event with label: MAPPINGS, download the file list.

Request download URLs and process files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
async function handleFileListUploaded(event) {
  const { key, label, movementDetails } = event;

  if (label !== 'MAPPINGS') {
    throw new Error(`Expected label 'MAPPINGS', got '${label}'`);
  }

  // 1. Request presigned URL for the file list
  const listingUrl = await getFileListingUrl(key);

  // 2. Download the file list
  const fileListing = await downloadFileListing(listingUrl);

  // 3. Batch fetch download URLs for all event files (up to 100 at a time)
  const fileIds = fileListing.map(ef => ef.fileId);
  const urlsMap = await getEventFileUrls(fileIds);

  // 4. Process files in parallel (10 at a time for performance)
  const CONCURRENCY = 10;
  for (let i = 0; i < fileListing.length; i += CONCURRENCY) {
    const batch = fileListing.slice(i, i + CONCURRENCY);
    await Promise.all(
      batch.map(eventFile =>
        processEventFile(eventFile, urlsMap.get(eventFile.fileId), movementDetails)
      )
    );
  }
}

async function getFileListingUrl(fileKey) {
  const response = await fetch(
    'https://api.atlassian.com/app/migration/forge/v1/file-listing/download-url',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}` // Get from Forge context
      },
      body: JSON.stringify({ fileKey })
    }
  );

  const { url } = await response.json();
  return url;
}

async function downloadFileListing(presignedUrl) {
  // This file can be up to 5GB, make sure your production code can handle this
  const response = await fetch(presignedUrl);

  // Stream the JSONL file line-by-line to handle large listings efficiently
  const eventFiles = [];
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();

    if (value) {
      buffer += decoder.decode(value, { stream: true });

      // Process complete lines
      const lines = buffer.split('\n');
      buffer = lines.pop(); // Keep incomplete line in buffer

      for (const line of lines) {
        if (line.trim()) {
          eventFiles.push(JSON.parse(line));
        }
      }
    }

    if (done) {
      // Process any remaining data
      if (buffer.trim()) {
        eventFiles.push(JSON.parse(buffer));
      }
      break;
    }
  }

  return eventFiles;
}

async function getEventFileUrls(fileIds) {
  // Batch fetch up to 100 URLs at once (API limit)
  const BATCH_SIZE = 100;
  const urlsMap = new Map();

  for (let i = 0; i < fileIds.length; i += BATCH_SIZE) {
    const batch = fileIds.slice(i, i + BATCH_SIZE);

    const response = await fetch(
      'https://api.atlassian.com/app/migration/forge/v1/event-file/urls',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${accessToken}`
        },
        body: JSON.stringify({ eventFileIds: batch })
      }
    );

    const { eventFiles } = await response.json();
    eventFiles.forEach(({fileId, presignedUrl}) => {
      urlsMap.set(fileId, presignedUrl);
    });
  }

  return urlsMap;
}

Step 4: Download and process event files

Each event file contains mapping records in JSONL format.

Download event files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async function processEventFile(eventFile, presignedUrl, movementDetails) {
  const { fileId } = eventFile;

  console.log(`Processing event file: ${fileId}`);

  try {
    // 1. Download the event file (URL already fetched in batch)
    const mappingRecords = await downloadEventFile(presignedUrl);

    // 2. Process the mappings
    await importMappings(mappingRecords, movementDetails);

    // 3. Acknowledge successful processing
    await acknowledgeEventFile(fileId, movementDetails.appDataMovementId, 'SUCCESS');

  } catch (error) {
    console.error(`Failed to process ${fileId}:`, error);
    await acknowledgeEventFile(fileId, movementDetails.appDataMovementId, 'FAILED');
    throw error;
  }
}

async function downloadEventFile(presignedUrl) {
  const response = await fetch(presignedUrl);
  const text = await response.text();

  // Parse JSONL format
  const lines = text.trim().split('\n');

  // First line is metadata header
  const metadata = JSON.parse(lines[0]);
  console.log(`Event file metadata:`, metadata);

  // Remaining lines are mapping records
  return lines.slice(1).map(line => JSON.parse(line));
}

Process mapping records

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async function importMappings(records, movementDetails) {
  const { cloudUrl } = movementDetails.migrationDetails;

  for (const record of records) {
    const { sourceId, targetId, entityType } = record;

    // Store the mapping in your app's database
    await storeMappingInYourDatabase({
      cloudUrl,
      dcId: sourceId,
      cloudId: targetId,
      entityType: entityType
    });

    console.log(`Mapped ${entityType}: ${sourceId} → ${targetId}`);
  }

  console.log(`Imported ${records.length} mappings`);
}

// Example mapping record structure:
// {
//   "sourceId": "10001",           // DC issue ID
//   "targetId": "10234",           // Cloud issue ID
//   "entityType": "jira:issue",
//   "namespace": "ari:cloud:jira::site/abc123"
// }

Step 5: Acknowledge processing

After successfully importing the mappings, acknowledge the event file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function acknowledgeEventFile(eventFileId, appDataMovementId, status) {
  await fetch(
    'https://api.atlassian.com/app/migration/forge/v1/event-files/status',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`
      },
      body: JSON.stringify({
        appDataMovementId,
        eventFileIds: [eventFileId],
        status // 'SUCCESS' or 'FAILED'
      })
    }
  );

  console.log(`Acknowledged event file ${eventFileId} with status: ${status}`);
}

Important:

  • Only acknowledge after data is durably stored (if you are storing them)
  • Use SUCCESS for files processed successfully
  • Use FAILED only for unrecoverable errors (this fails the entire migration)
  • Each acknowledgement resets the 15-minute inactivity timeout

Step 6: Test your integration

Test the integration by running an actual DC→Cloud migration.

Deploy your changes

Before testing, deploy all code changes to the development environment:

1
2
forge deploy --environment development

Always test your SAM integration in the development environment before promoting to staging or production.

Prepare test data

  1. On your Data Center test instance, create some test content:
    • Create a few Jira issues (note their keys, e.g., "PROJ-123")
    • Record the DC issue IDs (visible in the issue URL or database)
  2. Ensure both your DC app and cloud app are installed on their respective instances

Run a migration

  1. Open Jira Cloud Migration Assistant (JCMA) on your DC instance
  2. Start a new migration to your cloud test site
  3. Select a project or subset of data to migrate
  4. Ensure your app is included in the migration (check app selection settings)
  5. Run the migration and wait for it to complete

Important timing: For mappings, the event-file-list-uploaded notification is sent after the product migration completes. This is because DC-to-Cloud ID mappings are only created once Jira has migrated issues/pages to cloud.

Migration flow for mappings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. JCMA starts migration
   ↓
2. Product migrates issues/pages to cloud
   ↓ (DC IDs now mapped to Cloud IDs)
3. Platform generates mapping files
   ↓
4. event-file-list-uploaded sent to your cloud app (label: MAPPINGS)
   ↓
5. Your cloud app downloads and processes mappings
   ↓
6. Your cloud app acknowledges event files
   ↓
7. Migration completes

Verify the flow

Check your cloud app logs for:

1
2
3
4
5
6
7
✓ Received migration event: event-file-list-uploaded
✓ File list uploaded - Label: MAPPINGS, IsComplete: true
✓ Processing event file: event_file/abc-123-def
✓ Mapped jira:issue: 10001 → 10234
✓ Imported 150 mappings
✓ Acknowledged event file with status: SUCCESS

Validate mappings

Query your cloud app's database to confirm:

  • Mappings were stored correctly
  • DC issue IDs from your test data map to the correct cloud issue IDs
  • Check a few examples manually:
    • Find a DC issue ID you recorded
    • Verify it maps to the correct cloud issue ID (check in Jira Cloud)
  • All expected entity types are present (e.g., jira:issue, jira:project)

Next steps

Congratulations! You now have a working SAM integration. 🎉

Expand your integration:

Learn more:

Production readiness:

  • Handle all migration lifecycle events (listener-triggered, transfer-settled, transfer-cancellation-requested)
  • Implement retry logic for API calls
  • Add comprehensive error logging
  • Test with larger datasets
  • Review Migration readiness checklist
  • Each file list event can be up to 5GB, make sure you can handle that size

Troubleshooting

No event-file-list-uploaded event received?

  • Verify SAM is enabled with your data center app
  • Check that manifest includes migration:managedExports
  • Ensure app is deployed to the test site
  • Confirm app is selected in the migration
  • Ensure Forge app environment matches between DC and Cloud (i.e. development in both DC and Cloud)

isComplete: false on all events?

  • Wait for the event where isComplete: true before considering data complete
  • You can start processing earlier, but don't assume you have all data until isComplete: true

Need help?


Rate this page: