diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md
index 50e954c04223e..bb4b1a2a29f80 100644
--- a/packages/connect-react/CHANGELOG.md
+++ b/packages/connect-react/CHANGELOG.md
@@ -1,6 +1,10 @@
 
 # Changelog
 
+# [1.0.0-preview.8] - 2024-12-09
+
+- Disabled submit button when form is incomplete
+
 # [1.0.0-preview.7] - 2024-12-05
 
 - Use proper casing for `stringOptions` now that configure prop is properly async
diff --git a/packages/connect-react/examples/nextjs/package-lock.json b/packages/connect-react/examples/nextjs/package-lock.json
index d4fed8c62dcb0..0351814ee6464 100644
--- a/packages/connect-react/examples/nextjs/package-lock.json
+++ b/packages/connect-react/examples/nextjs/package-lock.json
@@ -23,10 +23,10 @@
     },
     "../..": {
       "name": "@pipedream/connect-react",
-      "version": "1.0.0-preview.6",
+      "version": "1.0.0-preview.8",
       "license": "MIT",
       "dependencies": {
-        "@pipedream/sdk": "^1.0.6",
+        "@pipedream/sdk": "workspace:^",
         "@tanstack/react-query": "^5.59.16",
         "lodash.isequal": "^4.5.0",
         "react-markdown": "^9.0.1",
diff --git a/packages/connect-react/examples/nextjs/src/app/page.tsx b/packages/connect-react/examples/nextjs/src/app/page.tsx
index 283e783623c1f..6de7fa73bebd0 100644
--- a/packages/connect-react/examples/nextjs/src/app/page.tsx
+++ b/packages/connect-react/examples/nextjs/src/app/page.tsx
@@ -30,6 +30,17 @@ export default function Home() {
           componentKey="slack-send-message"
           configuredProps={configuredProps}
           onUpdateConfiguredProps={setConfiguredProps}
+          onSubmit={async () => {
+            try {
+              await client.actionRun({
+                userId,
+                actionId: "slack-send-message",
+                configuredProps,
+              });
+            } catch (error) {
+              console.error("Action run failed:", error);
+            }
+          }}
         />
       
     >
diff --git a/packages/connect-react/src/components/ControlSubmit.tsx b/packages/connect-react/src/components/ControlSubmit.tsx
index 83e6e3f789769..a25f39c7f8ae8 100644
--- a/packages/connect-react/src/components/ControlSubmit.tsx
+++ b/packages/connect-react/src/components/ControlSubmit.tsx
@@ -8,16 +8,22 @@ export type ControlSubmitProps = {
 
 export function ControlSubmit(props: ControlSubmitProps) {
   const { form } = props;
-  const { submitting } = form;
+  const {
+    propsNeedConfiguring, submitting,
+  } = form;
 
   const {
     getProps, theme,
   } = useCustomize();
-  const baseStyles: CSSProperties = {
+  const baseStyles = (disabled: boolean): CSSProperties => ({
     width: "fit-content",
     textTransform: "capitalize",
-    backgroundColor: theme.colors.primary,
-    color: theme.colors.neutral0,
+    backgroundColor: disabled
+      ? theme.colors.neutral10
+      : theme.colors.primary,
+    color: disabled
+      ? theme.colors.neutral40
+      : theme.colors.neutral0,
     padding: `${theme.spacing.baseUnit * 1.75}px ${
       theme.spacing.baseUnit * 16
     }px`,
@@ -29,9 +35,9 @@ export function ControlSubmit(props: ControlSubmitProps) {
       ? 0.5
       : undefined,
     margin: "0.5rem 0 0 0",
-  };
+  });
 
   return ;
+    : "Submit"} {...getProps("controlSubmit", baseStyles(propsNeedConfiguring.length || submitting), props)} disabled={propsNeedConfiguring.length || submitting} />;
 }
diff --git a/packages/connect-react/src/components/InternalField.tsx b/packages/connect-react/src/components/InternalField.tsx
index a3bee58dac710..047d714914891 100644
--- a/packages/connect-react/src/components/InternalField.tsx
+++ b/packages/connect-react/src/components/InternalField.tsx
@@ -3,6 +3,7 @@ import { FormFieldContext } from "../hooks/form-field-context";
 import { useFormContext } from "../hooks/form-context";
 import { Field } from "./Field";
 import { useApp } from "../hooks/use-app";
+import { useEffect } from "react";
 
 type FieldInternalProps = {
   prop: T;
@@ -14,7 +15,7 @@ export function InternalField({
 }: FieldInternalProps) {
   const formCtx = useFormContext();
   const {
-    id: formId, configuredProps, setConfiguredProp,
+    id: formId, configuredProps, registerField, setConfiguredProp,
   } = formCtx;
 
   const appSlug = prop.type === "app" && "app" in prop
@@ -44,7 +45,9 @@ export function InternalField({
       app, // XXX fix ts
     },
   };
-
+  useEffect(() => registerField(fieldCtx), [
+    fieldCtx,
+  ])
   return (
     
       
diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx
index 19cfde742b473..374203ca4d8fd 100644
--- a/packages/connect-react/src/hooks/form-context.tsx
+++ b/packages/connect-react/src/hooks/form-context.tsx
@@ -4,10 +4,12 @@ import {
 import isEqual from "lodash.isequal";
 import { useQuery } from "@tanstack/react-query";
 import type {
-  ComponentReloadPropsOpts, ConfigurableProp, ConfigurableProps, ConfiguredProps, V1Component,
+  ComponentReloadPropsOpts, ConfigurableProp, ConfigurableProps, ConfiguredProps, V1Component, PropValue,
 } from "@pipedream/sdk";
 import { useFrontendClient } from "./frontend-client-context";
 import type { ComponentFormProps } from "../components/ComponentForm";
+import type { FormFieldContext } from "./form-field-context";
+import { appPropError } from "./use-app";
 
 export type DynamicProps = { id: string; configurableProps: T; }; // TODO
 
@@ -17,12 +19,15 @@ export type FormContext = {
   configuredProps: ConfiguredProps;
   dynamicProps?: DynamicProps; // lots of calls require dynamicProps?.id, so need to expose
   dynamicPropsQueryIsFetching?: boolean;
+  fields: Record>;
   id: string;
   isValid: boolean;
   optionalPropIsEnabled: (prop: ConfigurableProp) => boolean;
   optionalPropSetEnabled: (prop: ConfigurableProp, enabled: boolean) => void;
   props: ComponentFormProps;
+  propsNeedConfiguring: string[];
   queryDisabledIdx?: number;
+  registerField: (field: FormFieldContext) => void;
   setConfiguredProp: (idx: number, value: unknown) => void; // XXX type safety for value (T will rarely be static right?)
   setSubmitting: (submitting: boolean) => void;
   submitting: boolean;
@@ -64,6 +69,10 @@ export const FormContextProvider = ({
     queryDisabledIdx,
     setQueryDisabledIdx,
   ] = useState(0);
+  const [
+    fields,
+    setFields,
+  ] = useState>>({});
   const [
     submitting,
     setSubmitting,
@@ -129,6 +138,16 @@ export const FormContextProvider = ({
     enabled: reloadPropIdx != null, // TODO or props.dynamicPropsId && !dynamicProps
   });
 
+  const [
+    propsNeedConfiguring,
+    setPropsNeedConfiguring,
+  ] = useState([]);
+  useEffect(() => {
+    checkPropsNeedConfiguring()
+  }, [
+    configuredProps,
+  ]);
+
   // XXX fix types of dynamicProps, props.component so this type decl not needed
   let configurableProps: T = dynamicProps?.configurableProps || formProps.component.configurable_props || [];
   if (propNames?.length) {
@@ -147,7 +166,7 @@ export const FormContextProvider = ({
 
   // these validations are necessary because they might override PropInput for number case for instance
   // so can't rely on that base control form validation
-  const propErrors = (prop: ConfigurableProp, value: unknown): string[] => {
+  const propErrors = (prop: ConfigurableProp, value: unknown): string[] => {
     const errs: string[] = [];
     if (value === undefined) {
       if (!prop.optional) {
@@ -173,7 +192,14 @@ export const FormContextProvider = ({
         errs.push("not a string");
       }
     } else if (prop.type === "app") {
-      // TODO need to know about auth type
+      const field = fields[prop.name]
+      if (field) {
+        const app = field.extra.app
+        const err = appPropError({ value, app })
+        if (err) errs.push(err)
+      } else {
+        errs.push("field not registered")
+      }
     }
     return errs;
   };
@@ -302,6 +328,27 @@ export const FormContextProvider = ({
     setEnabledOptionalProps(newEnabledOptionalProps);
   };
 
+  const checkPropsNeedConfiguring = () => {
+    const _propsNeedConfiguring = []
+    for (const prop of configurableProps) {
+      if (!prop || prop.optional || prop.hidden) continue
+      const value = configuredProps[prop.name as keyof ConfiguredProps]
+      const errors = propErrors(prop, value)
+      if (errors.length) {
+        _propsNeedConfiguring.push(prop.name)
+      }
+    }
+    // propsNeedConfiguring.splice(0, propsNeedConfiguring.length, ..._propsNeedConfiguring)
+    setPropsNeedConfiguring(_propsNeedConfiguring)
+  }
+
+  const registerField = (field: FormFieldContext) => {
+    setFields((fields) => {
+      fields[field.prop.name] = field
+      return fields
+    });
+  };
+
   // console.log("***", configurableProps, configuredProps)
   const value: FormContext = {
     id,
@@ -313,9 +360,12 @@ export const FormContextProvider = ({
     configuredProps,
     dynamicProps,
     dynamicPropsQueryIsFetching,
+    fields,
     optionalPropIsEnabled,
     optionalPropSetEnabled,
+    propsNeedConfiguring,
     queryDisabledIdx,
+    registerField,
     setConfiguredProp,
     setSubmitting,
     submitting,
diff --git a/packages/connect-react/src/hooks/use-app.tsx b/packages/connect-react/src/hooks/use-app.tsx
index a41252d615f12..4dca554fb3246 100644
--- a/packages/connect-react/src/hooks/use-app.tsx
+++ b/packages/connect-react/src/hooks/use-app.tsx
@@ -2,7 +2,10 @@ import {
   useQuery, type UseQueryOptions,
 } from "@tanstack/react-query";
 import { useFrontendClient } from "./frontend-client-context";
-import type { AppRequestResponse } from "@pipedream/sdk";
+import type {
+  AppRequestResponse, AppResponse, ConfigurablePropApp,
+  PropValue,
+} from "@pipedream/sdk";
 
 /**
  * Get details about an app
@@ -23,3 +26,65 @@ export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit & {
+  oauth_access_token?: string
+}
+
+function getCustomFields(app: AppResponse): AppCustomField[] {
+  const isOauth = app.auth_type === "oauth"
+  const userDefinedCustomFields = JSON.parse(app.custom_fields_json || "[]")
+  if ("extracted_custom_fields_names" in app && app.extracted_custom_fields_names) {
+    const extractedCustomFields = ((app as AppResponseWithExtractedCustomFields).extracted_custom_fields_names || []).map(
+      (name) => ({
+        name,
+      }),
+    )
+    userDefinedCustomFields.push(...extractedCustomFields)
+  }
+  return userDefinedCustomFields.map((cf: AppCustomField) => {
+    return {
+      ...cf,
+      // if oauth, treat all as optional (they are usually needed for getting access token)
+      optional: cf.optional || isOauth,
+    }
+  })
+}
+
+export function appPropError(opts: { value: any, app: AppResponse | undefined }): string | undefined {
+  const { app, value } = opts
+  if (!app) {
+    return "app field not registered"
+  }
+  if (!value) {
+    return "no app configured"
+  }
+  if (typeof value !== "object") {
+    return "not an app"
+  }
+  const _value = value as PropValue<"app">
+  if ("authProvisionId" in _value && !_value.authProvisionId) {
+    if (app.auth_type) {
+      if (app.auth_type === "oauth" && !(_value as OauthAppPropValue).oauth_access_token) {
+        return "missing oauth token"
+      }
+      if (app.auth_type === "oauth" || app.auth_type === "keys") {
+        for (const cf of getCustomFields(app)) {
+          if (!cf.optional && !_value[cf.name]) {
+            return "missing custom field"
+          }
+        }
+      }
+      return "no auth provision configured"
+    }
+  }
+}