Credential name auto-increment seems global for custom credentials, but per-person for some built-ins (OpenAI)

I’m seeing inconsistent credential naming behavior in n8n and want to confirm if this is expected.

What I observe

  • For my custom credential type (myApi), new credentials are auto-named globally across the instance:

    • User A creates personal credentials: MyApi Account, MyApi Account 2

    • User B (who cannot see User A’s personal credentials) creates one and gets: MyApi Account 3

  • But e.g. for OpenAI credentials, the counter appears to be personal/project-scoped (it does not continue from other users’ credentials in the same way).

Why this is confusing

Credential visibility is personal/project-based, but the generated name suffix for my custom type appears to use a global counter. This makes naming look “leaky” across users even though credentials are not shared.

Question

  • Is this difference between built-in (e.g. OpenAI) and custom credential types expected?

  • Are there multiple credential creation/name-generation flows in n8n that explain this?

  • Is there any way for a custom credential type to use per-user/per-project name generation instead of global suffixing?

"n8n-workflow": "^1.120.13"

Welcome to the n8n community @Jankaz
Did you manage to verify if the OpenAI credential is being created by the same flow as your custom credential (personal project vs shared project vs owner’s global project)?
My understanding is that your problem lies in the internal creation/naming path that’s being used. Can you share a simple repro comparing custom credential vs OpenAI credential, including project, user role, generated names, and numbering behavior between users?

I checked the requests in network and both use https://n8n.stg.olx.org/rest/credentialswith the same payload structure. Both with the same projectId. The only difference is that payload for openai has additional field uiContext: "credentials_list" but I guess it’s not the case

MyApi.credentials.ts

export class MyApi implements ICredentialType {
  name = "myApi";

  displayName = "My API";

  properties: INodeProperties[] = [
    {
      displayName: "Platform Base URL",
      name: "baseUrl",
      type: "string",
      default: "http://myapi.com",
      placeholder: "http://myapi.com",
      required: true,
      hint: "Click `Save` to authenticate.",
      description: "",
    },
    {
      displayName: "Secret",
      name: "adminToken",
      type: "hidden",
      typeOptions: {
        password: true,
      },
      default: "",
      description:
        "Populated through n8n credential overwrites.",
    },
  ];

  test: ICredentialTestRequest = {
    request: {
      method: "GET",
      baseURL: "={{$credentials.baseUrl}}",
      url: "/health",
    },
  };
}

import type {
  ICredentialDataDecryptedObject,
  ICredentialTestRequest,
  ICredentialType,
  IHttpRequestOptions,
  INodeProperties,
} from 'n8n-workflow';

export class OpenAiApi implements ICredentialType {
  name = 'openAiApi';

  displayName = 'OpenAI';

  documentationUrl = 'openai';

  properties: INodeProperties[] = [
   {
    displayName: 'API Key',
    name: 'apiKey',
    type: 'string',
    typeOptions: { password: true },
    required: true,
    default: '',
   },
   {
    displayName: 'Organization ID (optional)',
    name: 'organizationId',
    type: 'string',
    default: '',
    hint: 'Only required if you belong to multiple organisations',
    description:
     "For users who belong to multiple organizations, you can set which organization is used for an API request. Usage from these API requests will count against the specified organization's subscription quota.",
   },
   {
    displayName: 'Base URL',
    name: 'url',
    type: 'string',
    default: 'https://api.openai.com/v1',
    description: 'Override the default base URL for the API',
   },
   {
    displayName: 'Add Custom Header',
    name: 'header',
    type: 'boolean',
    default: false,
   },
   {
    displayName: 'Header Name',
    name: 'headerName',
    type: 'string',
    displayOptions: {
     show: {
      header: [true],
     },
    },
    default: '',
   },
   {
    displayName: 'Header Value',
    name: 'headerValue',
    type: 'string',
    typeOptions: {
     password: true,
    },
    displayOptions: {
     show: {
      header: [true],
     },
    },
    default: '',
   },
  ];

  test: ICredentialTestRequest = {
   request: {
    baseURL: '={{$credentials?.url}}',
    url: '/models',
   },
  };

  async authenticate(
   credentials: ICredentialDataDecryptedObject,
   requestOptions: IHttpRequestOptions,
  ): Promise<IHttpRequestOptions> {
   requestOptions.headers ??= {};

   requestOptions.headers['Authorization'] = `Bearer ${credentials.apiKey}`;
   requestOptions.headers['OpenAI-Organization'] = credentials.organizationId;

   if (
    credentials.header &&
    typeof credentials.headerName === 'string' &&
    credentials.headerName &&
    typeof credentials.headerValue === 'string'
   ) {
    requestOptions.headers[credentials.headerName] = credentials.headerValue;
   }

   return requestOptions;
  }
}

@Jankaz
Based on the backend code, it looks like POST /rest/credentials persists the name received in the payload, so I’d investigate the name generation on the frontend before the request is sent. If OpenAI and the custom credential reach the same endpoint with the same projectId but already with different names, the difference is probably in the UI flow that calculates the default credential name, not in the backend’s save() method.

Thanks - agreed that POST `/rest/credential` just saves the provided name.
In network traces, both OpenAI and custom credential also call `GET /rest/credentials/new?name=` before POST.
So naming seems to be determined by (a) frontend base name + (b) backend unique-name generation in /credentials/new.
This suggests the difference comes from existing-name matching for each prefix (OpenAI account% vs GAIP account%), rather than from save() or uiContext.

So.. what’s the difficulty in renaming the credential? :slight_smile:

I’ve been working with n8n for about 4 years, and I’ve seen that leaving the base name leads to issues with updating credentials later or determining what is what

there’s no difficulty, but it’s confusing

It’s normal @Jankaz, once you figure out your way of understanding the issue it will get easier. It’s not something configurable, they are actually different visually.

I couldn’t find any parameter to change the automatic name generation logic

The separation by user/project exists at the permission and visibility level of credentials, via RBAC/Projects, but that doesn’t mean the automatic name counter is also scoped by project. The RBAC documentation talks about access to workflows and credentials by project, not about naming algorithm.

as suggested in the thread, the workaround is to manually name the credentials with project/user in the name.