pgkiln 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/SECURITY.md +7 -0
  2. package/db/migrations/119_workspace_home.sql +30 -0
  3. package/db/migrations/120_datasets.sql +87 -0
  4. package/db/migrations/121_ai_agents.sql +94 -0
  5. package/db/migrations/122_team_development.sql +186 -0
  6. package/db/migrations/123_workflow_outcomes.sql +407 -0
  7. package/db/migrations/124_component_details.sql +51 -0
  8. package/docs/apex-feature-parity.md +15 -12
  9. package/docs/guide/01-installation.md +9 -3
  10. package/docs/guide/03-builder.md +211 -11
  11. package/docs/guide/04-pages-and-regions.md +96 -22
  12. package/docs/guide/05-items.md +7 -1
  13. package/docs/guide/06-processing.md +123 -8
  14. package/docs/guide/07-dynamic-actions.md +34 -0
  15. package/docs/guide/09-reference.md +2 -1
  16. package/docs/guide/11-from-apex.md +2 -1
  17. package/docs/guide/12-development.md +70 -11
  18. package/docs/guide/20-ai-agents.md +4 -2
  19. package/examples/hr/README.md +2 -0
  20. package/examples/hr/hr_63_datasets.sql +29 -0
  21. package/examples/hr/hr_64_ai_agent.sql +97 -0
  22. package/examples/hr/hr_65_ask_reports.sql +21 -0
  23. package/examples/hr/hr_66_team_development.sql +58 -0
  24. package/examples/hr/hr_67_workflow_outcomes.sql +113 -0
  25. package/examples/hr/hr_68_component_details.sql +96 -0
  26. package/package.json +1 -1
  27. package/public/app.css +46 -0
  28. package/public/app.js +134 -20
  29. package/public/builder.css +116 -0
  30. package/public/builder.js +47 -0
  31. package/src/ai/chat.ts +18 -18
  32. package/src/ai/service.ts +35 -11
  33. package/src/ai/types.ts +7 -2
  34. package/src/app.ts +11 -1
  35. package/src/builder/advisor.ts +6 -3
  36. package/src/builder/ai-agents.ts +372 -0
  37. package/src/builder/ai-builder.ts +3 -3
  38. package/src/builder/ai.ts +13 -9
  39. package/src/builder/assistant.ts +22 -7
  40. package/src/builder/components.ts +38 -9
  41. package/src/builder/datasets.ts +472 -0
  42. package/src/builder/designer.ts +8 -3
  43. package/src/builder/feedback.ts +6 -1
  44. package/src/builder/home.ts +12 -7
  45. package/src/builder/instance.ts +2 -0
  46. package/src/builder/locks.ts +7 -1
  47. package/src/builder/region-help.ts +2 -2
  48. package/src/builder/reporter.ts +52 -3
  49. package/src/builder/routes.ts +7 -4
  50. package/src/builder/shared.ts +1 -1
  51. package/src/builder/subscriptions.ts +1 -0
  52. package/src/builder/team.ts +991 -0
  53. package/src/builder/ui.ts +8 -5
  54. package/src/builder/workflow-migrate.ts +210 -0
  55. package/src/builder/workflows.ts +29 -6
  56. package/src/builder/workspace-home.ts +285 -0
  57. package/src/builder/workspace-transfer.ts +1 -1
  58. package/src/builder/workspaces.ts +7 -4
  59. package/src/cli/replace.ts +7 -1
  60. package/src/datasets.ts +367 -0
  61. package/src/i18n/ar.ts +33 -0
  62. package/src/i18n/cs.ts +33 -0
  63. package/src/i18n/da.ts +33 -0
  64. package/src/i18n/de.ts +33 -0
  65. package/src/i18n/el.ts +33 -0
  66. package/src/i18n/es.ts +33 -0
  67. package/src/i18n/fi.ts +33 -0
  68. package/src/i18n/fr.ts +33 -0
  69. package/src/i18n/he.ts +33 -0
  70. package/src/i18n/it.ts +33 -0
  71. package/src/i18n/ja.ts +33 -0
  72. package/src/i18n/ko.ts +33 -0
  73. package/src/i18n/nb.ts +33 -0
  74. package/src/i18n/pl.ts +33 -0
  75. package/src/i18n/pt.ts +33 -0
  76. package/src/i18n/ru.ts +33 -0
  77. package/src/i18n/sv.ts +33 -0
  78. package/src/i18n/tr.ts +33 -0
  79. package/src/i18n/uk.ts +33 -0
  80. package/src/i18n/zh.ts +33 -0
  81. package/src/i18n.ts +66 -0
  82. package/src/instance.ts +3 -0
  83. package/src/metadata.ts +9 -3
  84. package/src/release-check.ts +139 -0
  85. package/src/reporting-app.ts +101 -0
  86. package/src/runtime/ai-agents.ts +365 -0
  87. package/src/runtime/ai-filter.ts +385 -107
  88. package/src/runtime/assistant.ts +64 -11
  89. package/src/runtime/context.ts +2 -0
  90. package/src/runtime/data-reporter.ts +199 -32
  91. package/src/runtime/ir-phrases.ts +812 -0
  92. package/src/runtime/ir-plan.ts +449 -0
  93. package/src/runtime/items.ts +1 -1
  94. package/src/runtime/regions.ts +6 -2
  95. package/src/runtime/render.ts +1 -1
  96. package/src/runtime/report.ts +1 -1
  97. package/src/runtime/tasks.ts +63 -12
  98. package/src/session.ts +2 -0
  99. package/src/subscriptions.ts +2 -1
  100. package/src/task-events.ts +232 -0
  101. package/src/team-samples.ts +74 -0
  102. package/src/team.ts +342 -0
  103. package/src/workflow.ts +135 -18
  104. package/src/workspace-export.ts +39 -4
package/src/ai/chat.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import OpenAI from 'openai';
3
3
  import { owner } from '../db.ts';
4
- import { decryptSecret } from '../secrets.ts';
5
4
  import { claudeError } from './anthropic.ts';
6
5
  import { openAiError } from './openai.ts';
7
- import { envKeyName, MAX_PROMPT_CHARS, MAX_SYSTEM_CHARS, serviceForApp, usageToday, logUsage, type AiService, type CallInfo } from './service.ts';
6
+ import { baseUrlFor, MAX_PROMPT_CHARS, MAX_SYSTEM_CHARS, serviceForApp, serviceKey, usageToday, logUsage, type AiService, type CallInfo } from './service.ts';
8
7
  import { AiError } from './types.ts';
9
8
 
10
9
  // Conversations with tools (AI assistant regions, src/runtime/assistant.ts):
@@ -23,7 +22,9 @@ import { AiError } from './types.ts';
23
22
  // service's effort, server-side refusal fallbacks. The assistant's content
24
23
  // is appended to the history unchanged (thinking blocks included): the
25
24
  // history only ever grows, as the API expects. OpenAI: Chat Completions with
26
- // strict function tools.
25
+ // strict function tools; also Gemini (its OpenAI-compatible endpoint) and
26
+ // OpenAI-compatible endpoints (121). An agent (src/runtime/ai-agents.ts)
27
+ // may set the reasoning effort and a lower output limit per call.
27
28
 
28
29
  export interface ChatTool {
29
30
  name: string;
@@ -47,6 +48,10 @@ export interface ChatRequest {
47
48
  /** at most this many rounds of tool calls (then the model must answer) */
48
49
  maxRounds: number;
49
50
  runTool(name: string, input: Record<string, unknown>): Promise<ToolOutcome>;
51
+ /** (121) an agent's reasoning effort: Claude's output_config.effort, OpenAI's reasoning_effort (else the service's own) */
52
+ effort?: 'low' | 'medium' | 'high' | null;
53
+ /** (121) an agent's output limit per call (never above the service's maximum) */
54
+ maxTokens?: number | null;
50
55
  }
51
56
 
52
57
  export interface ChatResult {
@@ -69,16 +74,9 @@ export interface ChatInfo {
69
74
  /** Tool results are cut to this many characters (the model sees that they were). */
70
75
  export const MAX_TOOL_RESULT_CHARS = 20_000;
71
76
 
72
- function apiKey(s: AiService) {
73
- let key: string | undefined;
74
- try {
75
- key = s.api_key_enc ? decryptSecret(s.api_key_enc) : process.env[envKeyName(s.provider)];
76
- } catch (e) {
77
- throw new AiError('config', `AI service ${s.name}: ${(e as Error).message}`);
78
- }
79
- if (!key) throw new AiError('config', `AI service ${s.name} has no API key (enter one in the builder, or set ${envKeyName(s.provider)} on the server).`);
80
- return key;
81
- }
77
+
78
+ /** The output limit of one call: the agent's, never above the service's. */
79
+ const outputLimit = (s: AiService, req: ChatRequest) => Math.min(s.max_tokens, req.maxTokens ?? s.max_tokens);
82
80
 
83
81
  const clip = (text: string) => (text.length > MAX_TOOL_RESULT_CHARS ? `${text.slice(0, MAX_TOOL_RESULT_CHARS)}\n[cut off: the result was longer]` : text);
84
82
 
@@ -114,7 +112,7 @@ async function metered<T extends { model: string; input: number; output: number
114
112
  const TOO_MANY_ROUNDS = 'The assistant needed more steps than it may take for one question: ask a simpler question, or start a new conversation.';
115
113
 
116
114
  async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], req: ChatRequest, info: ChatInfo): Promise<ChatResult> {
117
- const client = new Anthropic({ apiKey: apiKey(s), authToken: null, baseURL: s.base_url || undefined, timeout: s.timeout_s * 1000, maxRetries: 1 });
115
+ const client = new Anthropic({ apiKey: serviceKey(s), authToken: null, baseURL: baseUrlFor(s), timeout: s.timeout_s * 1000, maxRetries: 1 });
118
116
  const messages = [...(req.history as Anthropic.Beta.BetaMessageParam[]), { role: 'user', content: req.message } as Anthropic.Beta.BetaMessageParam];
119
117
  const tools: Anthropic.Beta.BetaTool[] = req.tools.map((t) => ({
120
118
  name: t.name, description: t.description, strict: true, input_schema: t.parameters as Anthropic.Beta.BetaTool.InputSchema,
@@ -126,11 +124,11 @@ async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], r
126
124
  try {
127
125
  m = await client.beta.messages.stream({
128
126
  model: s.model,
129
- max_tokens: s.max_tokens,
127
+ max_tokens: outputLimit(s, req),
130
128
  system: req.system,
131
129
  messages,
132
130
  ...(tools.length ? { tools } : {}),
133
- ...(s.effort ? { output_config: { effort: s.effort } } : {}),
131
+ ...((req.effort ?? s.effort) ? { output_config: { effort: (req.effort ?? s.effort)! } } : {}),
134
132
  ...(s.refusal_fallback ? { betas: ['server-side-fallback-2026-07-01'], fallbacks: 'default' as const } : {}),
135
133
  }).finalMessage();
136
134
  } catch (e) {
@@ -161,7 +159,7 @@ async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], r
161
159
  }
162
160
 
163
161
  async function openAiChat(s: AiService, limits: Parameters<typeof metered>[1], req: ChatRequest, info: ChatInfo): Promise<ChatResult> {
164
- const client = new OpenAI({ apiKey: apiKey(s), baseURL: s.base_url || undefined, timeout: s.timeout_s * 1000, maxRetries: 1 });
162
+ const client = new OpenAI({ apiKey: serviceKey(s), baseURL: baseUrlFor(s), timeout: s.timeout_s * 1000, maxRetries: 1 });
165
163
  const messages = [...(req.history as OpenAI.Chat.ChatCompletionMessageParam[]), { role: 'user', content: req.message } as OpenAI.Chat.ChatCompletionMessageParam];
166
164
  const tools: OpenAI.Chat.ChatCompletionTool[] = req.tools.map((t) => ({
167
165
  type: 'function', function: { name: t.name, description: t.description, parameters: t.parameters, strict: true },
@@ -175,8 +173,10 @@ async function openAiChat(s: AiService, limits: Parameters<typeof metered>[1], r
175
173
  model: s.model,
176
174
  // the system prompt is sent with each request, not kept in the history
177
175
  messages: [{ role: 'system', content: req.system }, ...messages],
178
- max_completion_tokens: s.max_tokens,
176
+ max_completion_tokens: outputLimit(s, req),
179
177
  ...(tools.length ? { tools } : {}),
178
+ // (121) an agent's reasoning effort; models without reasoning refuse it, so none is sent without one
179
+ ...(req.effort ? { reasoning_effort: req.effort } : {}),
180
180
  });
181
181
  } catch (e) {
182
182
  throw openAiError(e);
package/src/ai/service.ts CHANGED
@@ -4,7 +4,7 @@ import { ClaudeProvider } from './anthropic.ts';
4
4
  import { OpenAiProvider } from './openai.ts';
5
5
  import { AiError, type AiProvider, type GenerateRequest, type GenerateResult, type ProviderName } from './types.ts';
6
6
 
7
- // AI services (migration 060): an administrator configures them in the
7
+ // AI services (migration 060; Gemini and OpenAI-compatible endpoints: 121): an administrator configures them in the
8
8
  // builder (Workspace utilities → AI services) and allows each application
9
9
  // to use some of them, with optional limits per day. Every call goes
10
10
  // through generate() here: the service must exist, be enabled and be
@@ -35,33 +35,57 @@ export const DEFAULT_CLAUDE_MODEL = 'claude-opus-5-5';
35
35
  export const MAX_PROMPT_CHARS = 200_000;
36
36
  export const MAX_SYSTEM_CHARS = 50_000;
37
37
 
38
- const ENV_KEY: Record<ProviderName, string> = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY' };
38
+ const ENV_KEY: Record<ProviderName, string> = {
39
+ anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', gemini: 'GEMINI_API_KEY', openai_compatible: 'OPENAI_COMPATIBLE_API_KEY',
40
+ };
39
41
 
40
42
  export const envKeyName = (p: ProviderName) => ENV_KEY[p];
41
43
 
44
+ /** (121) The providers, as the builder names them. */
45
+ export const PROVIDER_LABELS: Record<ProviderName, string> = {
46
+ anthropic: 'Claude', openai: 'OpenAI', gemini: 'Gemini', openai_compatible: 'OpenAI-compatible',
47
+ };
48
+ export const providerLabel = (p: string) => PROVIDER_LABELS[p as ProviderName] ?? p;
49
+
50
+ /** (121) Google's OpenAI-compatible endpoint for Gemini (used when a Gemini service names no base URL). */
51
+ export const GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/';
52
+
53
+ /** The base URL a service's SDK client uses: its own, Gemini's default, or the SDK's default (undefined). */
54
+ export const baseUrlFor = (s: Pick<AiService, 'provider' | 'base_url'>) => s.base_url || (s.provider === 'gemini' ? GEMINI_BASE_URL : undefined);
55
+
42
56
  /** Where the service's key comes from: stored (encrypted), the server's environment, or nowhere. */
43
57
  export function keySource(s: Pick<AiService, 'provider' | 'api_key_enc'>): 'stored' | 'env' | 'missing' {
44
58
  if (s.api_key_enc) return 'stored';
45
59
  return process.env[ENV_KEY[s.provider]] ? 'env' : 'missing';
46
60
  }
47
61
 
62
+ /**
63
+ * The service's API key, decrypted only here (never stored elsewhere): the stored one, else the server's
64
+ * environment variable. An OpenAI-compatible endpoint may need none (a local Ollama): the SDK then sends a
65
+ * placeholder.
66
+ */
67
+ export function serviceKey(s: AiService): string {
68
+ let key: string | undefined;
69
+ try {
70
+ key = s.api_key_enc ? decryptSecret(s.api_key_enc) : process.env[ENV_KEY[s.provider]];
71
+ } catch (e) {
72
+ throw new AiError('config', `AI service ${s.name}: ${(e as Error).message}`);
73
+ }
74
+ if (!key && s.provider === 'openai_compatible') return 'none';
75
+ if (!key) throw new AiError('config', `AI service ${s.name} has no API key (enter one in the builder, or set ${ENV_KEY[s.provider]} on the server).`);
76
+ return key;
77
+ }
78
+
48
79
  export async function loadService(name: string): Promise<AiService | undefined> {
49
80
  return owner.one<AiService>('select * from meta.ai_service where name = $1', [name.toUpperCase()]);
50
81
  }
51
82
 
52
83
  /** The provider of a service, with its key (decrypted only here, never stored elsewhere). */
53
84
  export function providerFor(s: AiService): AiProvider {
54
- let apiKey: string | undefined;
55
- try {
56
- apiKey = s.api_key_enc ? decryptSecret(s.api_key_enc) : process.env[ENV_KEY[s.provider]];
57
- } catch (e) {
58
- throw new AiError('config', `AI service ${s.name}: ${(e as Error).message}`);
59
- }
60
- if (!apiKey) throw new AiError('config', `AI service ${s.name} has no API key (enter one in the builder, or set ${ENV_KEY[s.provider]} on the server).`);
61
85
  const conf = {
62
86
  model: s.model,
63
- apiKey,
64
- baseUrl: s.base_url,
87
+ apiKey: serviceKey(s),
88
+ baseUrl: baseUrlFor(s),
65
89
  timeoutMs: s.timeout_s * 1000,
66
90
  maxTokens: s.max_tokens,
67
91
  effort: s.effort,
package/src/ai/types.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  // The provider interface of pgkiln's AI features (src/ai/): one request in,
2
2
  // one answer out, whatever the provider. Providers live in anthropic.ts
3
3
  // (Claude, with the official @anthropic-ai/sdk) and openai.ts (the official
4
- // openai SDK); service.ts picks one for an AI service and adds the checks,
4
+ // openai SDK: OpenAI itself, Google Gemini through its OpenAI-compatible
5
+ // endpoint, and any OpenAI-compatible endpoint such as Ollama, vLLM or Azure
6
+ // OpenAI); service.ts picks one for an AI service and adds the checks,
5
7
  // limits and the usage log.
6
8
 
7
- export type ProviderName = 'anthropic' | 'openai';
9
+ export type ProviderName = 'anthropic' | 'openai' | 'gemini' | 'openai_compatible';
10
+
11
+ /** Providers that speak OpenAI's Chat Completions (the openai SDK). */
12
+ export const isOpenAiFamily = (p: ProviderName) => p !== 'anthropic';
8
13
 
9
14
  export interface ProviderConfig {
10
15
  /** the model, exactly as the developer entered it (never changed or downgraded) */
package/src/app.ts CHANGED
@@ -16,6 +16,7 @@ import { webSourceRoutes } from './builder/websources.ts';
16
16
  import { sqlRoutes } from './builder/sql.ts';
17
17
  import { automationRoutes } from './builder/automations.ts';
18
18
  import { workflowBuilderRoutes } from './builder/workflows.ts';
19
+ import { workflowMigrateRoutes } from './builder/workflow-migrate.ts';
19
20
  import { regionSettingsRoutes } from './builder/region-settings.ts';
20
21
  import { templateRoutes } from './builder/templates.ts';
21
22
  import { searchRoutes } from './builder/search.ts';
@@ -24,6 +25,7 @@ import { topSqlRoutes } from './builder/top-sql.ts';
24
25
  import { diagnosticsRoutes } from './builder/diagnostics.ts';
25
26
  import { ldapRoutes } from './builder/ldap.ts';
26
27
  import { aiRoutes } from './builder/ai.ts';
28
+ import { aiAgentRoutes } from './builder/ai-agents.ts';
27
29
  import { mailRoutes } from './builder/mail.ts';
28
30
  import { backupRoutes } from './builder/backups.ts';
29
31
  import { availabilityRoutes } from './builder/availability.ts';
@@ -52,7 +54,10 @@ import { blueprintRoutes } from './builder/blueprints.ts';
52
54
  import { accountRoutes } from './runtime/account.ts';
53
55
  import { taskRoutes } from './runtime/tasks.ts';
54
56
  import { dataReporterRoutes } from './runtime/data-reporter.ts';
57
+ import { datasetRoutes } from './builder/datasets.ts';
55
58
  import { reporterBuilderRoutes } from './builder/reporter.ts';
59
+ import { workspaceHomeRoutes } from './builder/workspace-home.ts';
60
+ import { teamRoutes } from './builder/team.ts';
56
61
  import { formDesignerRoutes } from './runtime/form-designer.ts';
57
62
  import { formDesignerBuilderRoutes } from './builder/form-designer.ts';
58
63
  import { builderErrorPages, builderServerError } from './builder/errors.ts';
@@ -213,6 +218,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
213
218
  await app.register(layoutRoutes);
214
219
  await app.register(automationRoutes);
215
220
  await app.register(workflowBuilderRoutes);
221
+ await app.register(workflowMigrateRoutes);
216
222
  await app.register(reportSettingsRoutes);
217
223
  await app.register(regionSettingsRoutes);
218
224
  await app.register(templateRoutes);
@@ -223,6 +229,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
223
229
  await app.register(diagnosticsRoutes);
224
230
  await app.register(ldapRoutes);
225
231
  await app.register(aiRoutes);
232
+ await app.register(aiAgentRoutes);
226
233
  await app.register(mailRoutes);
227
234
  await app.register(backupRoutes);
228
235
  await app.register(availabilityRoutes);
@@ -239,6 +246,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
239
246
  await app.register(codeEditorRoutes);
240
247
  await app.register(dataReporterRoutes);
241
248
  await app.register(reporterBuilderRoutes);
249
+ await app.register(datasetRoutes);
242
250
  await app.register(formDesignerRoutes);
243
251
  await app.register(formDesignerBuilderRoutes);
244
252
  await app.register(sampleDataRoutes);
@@ -247,6 +255,8 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
247
255
  await app.register(assistantBuilderRoutes);
248
256
  await app.register(aiBuilderRoutes);
249
257
  await app.register(blueprintRoutes);
250
- app.get('/', async (_req, reply) => reply.redirect('/builder'));
258
+ await app.register(workspaceHomeRoutes);
259
+ await app.register(teamRoutes);
260
+ app.get('/', async (_req, reply) => reply.redirect('/builder/home'));
251
261
  return app;
252
262
  }
@@ -6,7 +6,7 @@ import { devDbForApp } from '../workspace-db.ts';
6
6
  import { html, type Raw } from '../html.ts';
7
7
  import { icon } from '../icons.ts';
8
8
  import { templateProblem } from '../runtime/document.ts';
9
- import { invokeStepReferences, stepProblems, stepWarnings } from '../workflow.ts';
9
+ import { invokeStepReferences, isQuery, outcomeMap, stepProblems, stepWarnings } from '../workflow.ts';
10
10
  import { handlerProblems } from '../runtime/rest.ts';
11
11
  import { plpgsqlBlock } from '../runtime/rest-handlers.ts';
12
12
  import { processProblems } from '../runtime/processes.ts';
@@ -272,6 +272,8 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
272
272
  if (st?.type === 'sql' && typeof st.code === 'string') checks.push([`${v}step ${st.name}: code`, st.code, 'statements']);
273
273
  if (st?.type === 'task' && typeof st.owners === 'string' && st.owners.trim()) checks.push([`${v}step ${st.name}: owners`, st.owners, 'select']);
274
274
  if (st?.type === 'switch' && Array.isArray(st.cases)) for (const cs of st.cases) if (typeof cs?.when === 'string') checks.push([`${v}step ${st.name}: when`, cs.when, 'boolean']);
275
+ // (123) a send_mail step's recipients query
276
+ if (st?.type === 'send_mail' && typeof st.to === 'string' && isQuery(st.to)) checks.push([`${v}step ${st.name}: to`, st.to, 'select']);
275
277
  for (const [field, sql, shape] of checks) {
276
278
  checked++;
277
279
  const problem = await checkSql(c, sql, shape);
@@ -369,7 +371,8 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
369
371
  if (kind === 'rest_module') for (const problem of handlerProblems(row.handlers)) missing(e, 'Handlers (JSON)', problem);
370
372
  if (kind === 'workflow_definition') {
371
373
  const tasks = new Set(all.filter((x) => x.kind === 'task_definition').map((x) => x.row.name.toUpperCase()));
372
- for (const problem of stepProblems(row.steps, tasks)) missing(e, 'Steps (JSON)', problem);
374
+ const outcomes = outcomeMap(all.filter((x) => x.kind === 'task_definition').map((x) => x.row));
375
+ for (const problem of stepProblems(row.steps, tasks, { outcomes, drafts: false })) missing(e, 'Steps (JSON)', problem);
373
376
  for (const warning of stepWarnings(row.steps)) missing(e, 'Steps (JSON)', warning, 'warning');
374
377
  // invoke_api steps: REST data sources, parameters, web credentials, variables (sprint 32)
375
378
  const refs = {
@@ -382,7 +385,7 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
382
385
  for (const warning of invokeRefs.warnings) missing(e, 'Steps (JSON)', warning, 'warning');
383
386
  if (row.dev_version) {
384
387
  const field = `Steps of version ${row.dev_version} (development)`;
385
- for (const problem of stepProblems(row.dev_steps, tasks)) missing(e, field, problem, 'warning');
388
+ for (const problem of stepProblems(row.dev_steps, tasks, { outcomes })) missing(e, field, problem, 'warning');
386
389
  for (const warning of stepWarnings(row.dev_steps)) missing(e, field, warning, 'warning');
387
390
  const devRefs = invokeStepReferences(row.dev_steps, refs);
388
391
  for (const problem of [...devRefs.errors, ...devRefs.warnings]) missing(e, field, problem, 'warning');