Script Studio: Script Storage (SQL)

    A script can keep its own data between executions, in tables it creates itself. This is useful for anything a script needs to remember that does not belong in Jira as a field or a comment — a counter, a cache, a record of what has already been processed.

    Note: 📹 Video placeholder — creating a table, inserting a row on each run, and reading the totals back.

    1. Creating a table

    import { sql } from '@forge/sql';
    
    await sql.executeDDL(`
      CREATE TABLE IF NOT EXISTS note (
        id INT NOT NULL AUTO_INCREMENT,
        body VARCHAR(500) NOT NULL,
        created_at DATETIME NOT NULL,
        PRIMARY KEY (id)
      )
    `);
    

    IF NOT EXISTS is worth using consistently: a script that creates its tables at the start of every run should not fail on the second run because the table is already there.

    2. Reading and writing

    import { sql } from '@forge/sql';
    
    // A statement with parameters
    await sql.prepare('INSERT INTO note (body, created_at) VALUES (?, ?)')
      .bindParams('Escalated PROJ-142', new Date().toISOString())
      .execute();
    
    // A statement with none
    const { rows } = await sql.executeRaw('SELECT id, body FROM note ORDER BY id DESC LIMIT 10');
    for (const row of rows) {
      console.log(row.id, row.body);
    }
    

    Note: Always pass values through bindParams, using ? placeholders in the statement. Never build a statement by joining text together with a value that came from an issue, an event or a caller — that is how a script becomes vulnerable to SQL injection.

    3. Versioning a schema with the migration runner

    For a table whose structure will change over time, migrationRunner applies a named set of statements once each, in order, and remembers which have already run — useful when several scripts, or several versions of one script, share the same tables.

    import { migrationRunner } from '@forge/sql';
    
    migrationRunner
      .enqueue('001-create-note', `
        CREATE TABLE IF NOT EXISTS note (
          id INT NOT NULL AUTO_INCREMENT,
          body VARCHAR(500) NOT NULL,
          PRIMARY KEY (id)
        )
      `)
      .enqueue('002-add-created-at', `
        ALTER TABLE note ADD COLUMN created_at DATETIME NULL
      `);
    
    await migrationRunner.run();
    

    A second run with the same names applies only the ones that have not already succeeded.

    4. What is off-limits

    Scripts share the same database as Script Studio itself, which stores workspaces, run history and version control there. To keep the two apart:

    • Table names beginning studio_, and a short list of specific legacy names, are reserved. A statement that names one of them is refused before it reaches the database.
    • The database's own system schemas (information_schema, mysql, performance_schema, sys) are equally out of reach.

    5. Limits

    LimitValue
    SQL statements per execution40
    Length of a single statement8,000 characters
    Parameters per statement40

    These sit inside the execution's own time limit; see Security and Governance for how long a script may run depending on how it was started.

    Related pages